├── .github ├── ci-gradle.properties └── workflows │ └── Check.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── androiddevchallenge │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── cities.json │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── androiddevchallenge │ │ │ ├── Application.kt │ │ │ ├── MainActivity.kt │ │ │ ├── data │ │ │ ├── AQI.kt │ │ │ ├── City.kt │ │ │ ├── DayWeather.kt │ │ │ ├── HourlyWeather.kt │ │ │ ├── Padding.kt │ │ │ ├── Screen.kt │ │ │ ├── Temperature.kt │ │ │ ├── Weather.kt │ │ │ ├── WeatherForecasts.kt │ │ │ └── WeatherType.kt │ │ │ ├── di │ │ │ ├── AppSystemModule.kt │ │ │ ├── MoshiModule.kt │ │ │ └── NetworkModule.kt │ │ │ ├── repository │ │ │ ├── CitiesRepository.kt │ │ │ ├── MockRepository.kt │ │ │ ├── ProfileRepository.kt │ │ │ └── WeatherRepository.kt │ │ │ ├── ui │ │ │ ├── WeatherApp.kt │ │ │ ├── composable │ │ │ │ ├── AnimateAsState.kt │ │ │ │ ├── AnimationSpec.kt │ │ │ │ ├── Button.kt │ │ │ │ ├── Clickable.kt │ │ │ │ ├── Icon.kt │ │ │ │ ├── Image.kt │ │ │ │ ├── LinearProgressIndicator.kt │ │ │ │ ├── Pager.kt │ │ │ │ ├── RightTrapezoid.kt │ │ │ │ ├── Shape.kt │ │ │ │ └── Text.kt │ │ │ ├── details │ │ │ │ └── weather │ │ │ │ │ ├── Board.kt │ │ │ │ │ ├── Details.kt │ │ │ │ │ ├── GridItem.kt │ │ │ │ │ ├── SunriseSunset.kt │ │ │ │ │ ├── Transition.kt │ │ │ │ │ └── ViewModel.kt │ │ │ ├── home │ │ │ │ ├── ForecastsBar.kt │ │ │ │ ├── Home.kt │ │ │ │ ├── Pager.kt │ │ │ │ ├── TopBar.kt │ │ │ │ ├── Transition.kt │ │ │ │ └── ViewModel.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Typography.kt │ │ │ └── util │ │ │ ├── FontFamily.kt │ │ │ ├── LiveData.kt │ │ │ └── Math.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_aqi.xml │ │ ├── ic_baseline_navigate_next_24.xml │ │ ├── ic_humidity.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_menu.xml │ │ ├── ic_pressure.xml │ │ ├── ic_search.xml │ │ ├── ic_sun.xml │ │ ├── ic_temp_0.xml │ │ ├── ic_temp_1.xml │ │ ├── ic_temp_2.xml │ │ ├── ic_temp_3.xml │ │ ├── ic_temp_4.xml │ │ ├── ic_temp_5.xml │ │ ├── ic_temp_6.xml │ │ ├── ic_temp_7.xml │ │ ├── ic_temp_8.xml │ │ ├── ic_temp_9.xml │ │ ├── ic_temp_negative.xml │ │ ├── ic_temp_unit.xml │ │ ├── ic_visibility.xml │ │ └── ic_wind.xml │ │ ├── font │ │ ├── nunito_bold.ttf │ │ ├── nunito_extrabold.ttf │ │ ├── nunito_regular.ttf │ │ ├── nunito_semibold.ttf │ │ ├── rubik_light.ttf │ │ └── rubik_regular.ttf │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── cloudy.webp │ │ ├── fog.webp │ │ ├── haze.webp │ │ ├── ic_launcher.webp │ │ ├── ic_location.webp │ │ ├── sandstorm.webp │ │ ├── shower.webp │ │ ├── snow.webp │ │ ├── sunny.webp │ │ └── thundershower.webp │ │ ├── mipmap-mdpi │ │ ├── cloudy.webp │ │ ├── fog.webp │ │ ├── haze.webp │ │ ├── ic_launcher.webp │ │ ├── ic_location.webp │ │ ├── sandstorm.webp │ │ ├── shower.webp │ │ ├── snow.webp │ │ ├── sunny.webp │ │ └── thundershower.webp │ │ ├── mipmap-xhdpi │ │ ├── cloudy.webp │ │ ├── fog.webp │ │ ├── haze.webp │ │ ├── ic_launcher.webp │ │ ├── ic_location.webp │ │ ├── sandstorm.webp │ │ ├── shower.webp │ │ ├── snow.webp │ │ ├── sunny.webp │ │ └── thundershower.webp │ │ ├── mipmap-xxhdpi │ │ ├── cloudy.webp │ │ ├── fog.webp │ │ ├── haze.webp │ │ ├── ic_launcher.webp │ │ ├── ic_location.webp │ │ ├── sandstorm.webp │ │ ├── shower.webp │ │ ├── snow.webp │ │ ├── sunny.webp │ │ └── thundershower.webp │ │ ├── mipmap-xxxhdpi │ │ ├── cloudy.webp │ │ ├── fog.webp │ │ ├── haze.webp │ │ ├── ic_launcher.webp │ │ ├── ic_location.webp │ │ ├── sandstorm.webp │ │ ├── shower.webp │ │ ├── snow.webp │ │ ├── sunny.webp │ │ └── thundershower.webp │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── example │ └── androiddevchallenge │ └── ExampleUnitTest.kt ├── build.gradle ├── debug.keystore ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── icx.svg ├── results ├── screenshot_1.png ├── screenshot_2.png ├── screenshot_3.png ├── screenshot_4.png └── video.mp4 ├── settings.gradle └── spotless └── copyright.kt /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Copyright 2020 The Android Open Source Project 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | org.gradle.daemon=false 19 | org.gradle.parallel=true 20 | org.gradle.jvmargs=-Xmx5120m 21 | org.gradle.workers.max=2 22 | 23 | kotlin.incremental=false 24 | kotlin.compiler.execution.strategy=in-process 25 | -------------------------------------------------------------------------------- /.github/workflows/Check.yaml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 30 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Copy CI gradle.properties 18 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 19 | 20 | - name: Set up JDK 11 21 | uses: actions/setup-java@v1 22 | with: 23 | java-version: 11 24 | 25 | - name: Build project 26 | run: ./gradlew spotlessCheck assembleDebug lintDebug --stacktrace 27 | 28 | - name: Upload build outputs (APKs) 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: build-outputs 32 | path: app/build/outputs 33 | 34 | - name: Upload build reports 35 | if: always() 36 | uses: actions/upload-artifact@v2 37 | with: 38 | name: build-reports 39 | path: app/build/reports 40 | 41 | test: 42 | needs: build 43 | runs-on: macOS-latest # enables hardware acceleration in the virtual machine 44 | timeout-minutes: 30 45 | strategy: 46 | matrix: 47 | api-level: [23, 26, 29] 48 | 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v2 52 | 53 | - name: Copy CI gradle.properties 54 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 55 | 56 | - name: Set up JDK 11 57 | uses: actions/setup-java@v1 58 | with: 59 | java-version: 11 60 | 61 | - name: Run instrumentation tests 62 | uses: reactivecircus/android-emulator-runner@v2 63 | with: 64 | api-level: ${{ matrix.api-level }} 65 | arch: x86 66 | disable-animations: true 67 | script: ./gradlew connectedCheck --stacktrace 68 | 69 | - name: Upload test reports 70 | if: always() 71 | uses: actions/upload-artifact@v2 72 | with: 73 | name: test-reports 74 | path: app/build/reports 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Mac files 6 | .DS_Store 7 | 8 | # files for the dex VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # generated files 15 | bin/ 16 | gen/ 17 | 18 | # Ignore gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | proguard-project.txt 28 | 29 | # Eclipse files 30 | .project 31 | .classpath 32 | .settings/ 33 | 34 | # Android Studio/IDEA 35 | *.iml 36 | .idea -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Put title of your app here 2 | 3 | 4 | 5 | ![Workflow result](https://github.com///workflows/Check/badge.svg) 6 | 7 | 8 | ## :scroll: Description 9 | 10 | 11 | 12 | ## :bulb: Motivation and Context 13 | 14 | 15 | 16 | 17 | ## :camera_flash: Screenshots 18 | 19 | 20 | 21 | ## License 22 | ``` 23 | Copyright 2020 The Android Open Source Project 24 | 25 | Licensed under the Apache License, Version 2.0 (the "License"); 26 | you may not use this file except in compliance with the License. 27 | You may obtain a copy of the License at 28 | 29 | https://www.apache.org/licenses/LICENSE-2.0 30 | 31 | Unless required by applicable law or agreed to in writing, software 32 | distributed under the License is distributed on an "AS IS" BASIS, 33 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | See the License for the specific language governing permissions and 35 | limitations under the License. 36 | ``` -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle 3 | build/ 4 | 5 | captures 6 | 7 | /local.properties 8 | 9 | # IntelliJ .idea folder 10 | /.idea 11 | *.iml 12 | 13 | # General 14 | .DS_Store 15 | .externalNativeBuild -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | android { 9 | compileSdkVersion 30 10 | 11 | defaultConfig { 12 | applicationId "com.example.androiddevchallenge" 13 | minSdkVersion 23 14 | targetSdkVersion 30 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | signingConfigs { 22 | // We use a bundled debug keystore, to allow debug builds from CI to be upgradable 23 | debug { 24 | storeFile rootProject.file('debug.keystore') 25 | storePassword 'android' 26 | keyAlias 'androiddebugkey' 27 | keyPassword 'android' 28 | } 29 | } 30 | 31 | buildTypes { 32 | debug { 33 | signingConfig signingConfigs.debug 34 | } 35 | release { 36 | minifyEnabled false 37 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 38 | } 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = "1.8" 43 | } 44 | 45 | buildFeatures { 46 | compose true 47 | 48 | // Disable unused AGP features 49 | buildConfig false 50 | aidl false 51 | renderScript false 52 | resValues false 53 | shaders false 54 | } 55 | 56 | composeOptions { 57 | kotlinCompilerExtensionVersion compose_version 58 | } 59 | 60 | packagingOptions { 61 | // Multiple dependency bring these files in. Exclude them to enable 62 | // our test APK to build (has no effect on our AARs) 63 | excludes += "/META-INF/AL2.0" 64 | excludes += "/META-INF/LGPL2.1" 65 | } 66 | } 67 | 68 | dependencies { 69 | implementation 'androidx.core:core-ktx:1.3.2' 70 | implementation 'androidx.appcompat:appcompat:1.3.0-beta01' 71 | implementation "androidx.activity:activity-compose:1.3.0-alpha04" 72 | implementation "androidx.compose.runtime:runtime:$compose_version" 73 | implementation "androidx.compose.runtime:runtime-livedata:$compose_version" 74 | implementation "androidx.compose.material:material:$compose_version" 75 | implementation "androidx.compose.material:material-icons-extended:$compose_version" 76 | implementation "androidx.compose.ui:ui:$compose_version" 77 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 78 | implementation "androidx.navigation:navigation-compose:1.0.0-alpha09" 79 | implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha05" 80 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 81 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" 82 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" 83 | implementation 'androidx.hilt:hilt-navigation:1.0.0-beta01' 84 | implementation 'androidx.hilt:hilt-navigation-compose:1.0.0-alpha01' 85 | implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03' 86 | implementation "dev.chrisbanes.accompanist:accompanist-coil:$accompanist_version" 87 | implementation "dev.chrisbanes.accompanist:accompanist-insets:$accompanist_version" 88 | implementation "com.google.dagger:hilt-android:$google_hilt" 89 | implementation "com.meowbase.library:toolkit-core-android:0.1.18" 90 | implementation 'com.github.promeg:tinypinyin:2.0.3' 91 | implementation "com.squareup.moshi:moshi:1.11.0" 92 | 93 | kapt "androidx.hilt:hilt-compiler:1.0.0-beta01" 94 | kapt "com.google.dagger:hilt-android-compiler:$google_hilt" 95 | kapt "com.squareup.moshi:moshi-kotlin-codegen:1.11.0" 96 | 97 | testImplementation 'junit:junit:4.13.2' 98 | 99 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 100 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 101 | } -------------------------------------------------------------------------------- /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/androiddevchallenge/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge 17 | 18 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 19 | import androidx.test.ext.junit.runners.AndroidJUnit4 20 | import org.junit.Rule 21 | import org.junit.Test 22 | import org.junit.runner.RunWith 23 | 24 | /** 25 | * Instrumented test, which will execute on an Android device. 26 | * 27 | * See [testing documentation](http://d.android.com/tools/testing). 28 | */ 29 | @RunWith(AndroidJUnit4::class) 30 | class ExampleInstrumentedTest { 31 | @get:Rule 32 | val composeTestRule = createAndroidComposeRule() 33 | 34 | @Test 35 | fun sampleTest() { 36 | // Add instrumented tests here 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/Application.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2021 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 | package com.example.androiddevchallenge 17 | 18 | import android.app.Application 19 | import dagger.hilt.android.HiltAndroidApp 20 | 21 | /** 22 | * 程序的唯一的入口点 23 | * The only entry point to the application. 24 | * 25 | * @author 凛 (https://github.com/RinOrz) 26 | */ 27 | @HiltAndroidApp 28 | class MyApplication : Application() 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2021 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 | package com.example.androiddevchallenge 17 | 18 | import android.os.Bundle 19 | import androidx.activity.compose.setContent 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.core.view.WindowCompat.setDecorFitsSystemWindows 22 | import com.example.androiddevchallenge.ui.WeatherApp 23 | import com.example.androiddevchallenge.ui.theme.WeatherTheme 24 | import dagger.hilt.android.AndroidEntryPoint 25 | 26 | @AndroidEntryPoint 27 | class MainActivity : AppCompatActivity() { 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setDecorFitsSystemWindows(window, false) 31 | setContent { WeatherTheme { WeatherApp() } } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/AQI.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.data 17 | 18 | data class AQI( 19 | val value: Int, 20 | val pm25: Int, 21 | val pm10: Int, 22 | val so2: Int, 23 | val no2: Int, 24 | val co: Int, 25 | val o3: Int, 26 | ) { 27 | val level: Level 28 | get() = when (value) { 29 | in Level.Excellent.range -> Level.Excellent 30 | in Level.Good.range -> Level.Good 31 | in Level.LightlyPolluted.range -> Level.LightlyPolluted 32 | in Level.ModeratelyPolluted.range -> Level.ModeratelyPolluted 33 | in Level.HeavilyPolluted.range -> Level.HeavilyPolluted 34 | else -> Level.SeverelyPolluted 35 | } 36 | 37 | /** TODO Localization */ 38 | enum class Level(val range: IntRange, val category: String, val healthImplications: String) { 39 | Excellent(0..50, "Excellent", "No health implications."), 40 | Good( 41 | 51..100, 42 | "Good", 43 | "Some pollutants may slightly affect very few hypersensitive individuals." 44 | ), 45 | LightlyPolluted( 46 | 101..150, 47 | "Lightly Polluted", 48 | "Healthy people may experience slight irritations and sensitive individuals will be slightly affected to a larger extent." 49 | ), 50 | ModeratelyPolluted( 51 | 151..200, 52 | "Moderately Polluted", 53 | "Sensitive individuals will experience more serious conditions." 54 | ), 55 | HeavilyPolluted( 56 | 201..300, 57 | "Heavily Polluted", 58 | "Healthy people will commonly show symptoms. People with respiratory or heart diseases will be significantly affected and will experience reduced endurance in activities." 59 | ), 60 | SeverelyPolluted( 61 | 301..Int.MAX_VALUE, 62 | "Severely Polluted", 63 | "Healthy people will experience reduced endurance in activities and may also show noticeably strong symptoms. Other illnesses may be triggered in healthy people. Elders and the sick should remain indoors and avoid exercise. Healthy individuals should avoid outdoor activities." 64 | ), 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/City.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.data 17 | 18 | import com.github.promeg.pinyinhelper.Pinyin 19 | import com.squareup.moshi.FromJson 20 | import com.squareup.moshi.Json 21 | import com.squareup.moshi.JsonClass 22 | import com.squareup.moshi.JsonQualifier 23 | import com.squareup.moshi.ToJson 24 | 25 | /** 26 | * 关于城市的数据 27 | * 28 | * @author 凛 (https://github.com/RinOrz) 29 | */ 30 | @JsonClass(generateAdapter = true) 31 | data class City( 32 | @Json(name = "code") 33 | val code: Int, 34 | @Json(name = "name") @CityName 35 | val name: String, 36 | @Json(name = "provinceCode") 37 | val provinceCode: Int 38 | ) 39 | 40 | /** 我们在这里将 Json 中的城市名称与拼音互转 */ 41 | class CityNameAdapter { 42 | @FromJson @CityName 43 | fun getPinyin(name: String): String = Pinyin.toPinyin(name, "").toLowerCase().capitalize() 44 | 45 | @ToJson 46 | fun getChinese(@CityName name: String): String = TODO("Not supported yet.") 47 | } 48 | 49 | @JsonQualifier 50 | annotation class CityName 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/DayWeather.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.data 17 | 18 | /** 19 | * The weather data throughout the day. 20 | * 21 | * @author 凛 (https://github.com/RinOrz) 22 | * 23 | * @param hours24 The weather in the next 24 hours 24 | */ 25 | data class DayWeather( 26 | override val temperature: Temperature, 27 | override val type: WeatherType, 28 | override val aqi: AQI, 29 | val wind: Float, 30 | val humidity: Float, 31 | val pressure: Float, 32 | val visibility: Float, 33 | val sunrise: Long, 34 | val sunset: Long, 35 | val hours24: List, 36 | ) : Weather 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/HourlyWeather.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.data 17 | 18 | import com.meowbase.toolkit.formatTime 19 | 20 | /** 21 | * The weather data for an entire hour. 22 | * 23 | * @author 凛 (https://github.com/RinOrz) 24 | * 25 | * @param timestamp Represents the weather data belongs to this hour. 26 | */ 27 | data class HourlyWeather( 28 | override val temperature: Temperature, 29 | override val type: WeatherType, 30 | override val aqi: AQI, 31 | val timestamp: Long, 32 | ) : Weather { 33 | /** Hour number to which weather belongs. */ 34 | val hour: Int get() = formatTime(timestamp, pattern = "HH").toInt() 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/Padding.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.data 17 | 18 | import androidx.compose.runtime.compositionLocalOf 19 | import androidx.compose.ui.unit.Dp 20 | import androidx.compose.ui.unit.dp 21 | 22 | val LocalPadding = compositionLocalOf { Padding(16.dp, 16.dp) } 23 | 24 | fun Padding(horizontal: Dp, vertical: Dp): Padding = PaddingImpl(horizontal, vertical) 25 | 26 | interface Padding { 27 | val horizontal: Dp 28 | val vertical: Dp 29 | } 30 | 31 | private class PaddingImpl(override val horizontal: Dp, override val vertical: Dp) : Padding 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/Screen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2021 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 | package com.example.androiddevchallenge.data 17 | 18 | import androidx.navigation.NavController 19 | import androidx.navigation.NavOptionsBuilder 20 | import androidx.navigation.compose.navigate 21 | 22 | /** 23 | * 储存了所有 UI 屏幕的数据 24 | * Store all UI screen data. 25 | * 26 | * @author 凛 (https://github.com/RinOrz) 27 | */ 28 | sealed class Screen(val route: String) { 29 | object Home : Screen("Home") 30 | object Details : Screen("Details") { 31 | object Weather : Screen("Weather?cityCode={cityCode}&provinceCode={provinceCode}") 32 | object AQI : Screen("AQI") 33 | } 34 | } 35 | 36 | fun NavController.navigate( 37 | screen: Screen, 38 | vararg arguments: Pair, 39 | builder: NavOptionsBuilder.() -> Unit = {} 40 | ) = navigate( 41 | ( 42 | screen.route.run { 43 | var route = this 44 | arguments.forEach { 45 | route = route.replace("{${it.first}}", it.second.toString()) 46 | } 47 | route 48 | } 49 | ), 50 | builder 51 | ) 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/Temperature.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.data 17 | 18 | import com.example.androiddevchallenge.R 19 | 20 | /** 21 | * @author 凛 (https://github.com/RinOrz) 22 | * 23 | * @property realtime 实时温度 24 | * @property max 最高温度 25 | * @property min 最低温度 26 | */ 27 | data class Temperature( 28 | val realtime: Double, 29 | val max: Double, 30 | val min: Double 31 | ) { 32 | companion object { 33 | 34 | /** 获取特定的温度数字图标 */ 35 | fun getNumberIcon(char: Char) = when (char) { 36 | '0' -> R.drawable.ic_temp_0 37 | '1' -> R.drawable.ic_temp_1 38 | '2' -> R.drawable.ic_temp_2 39 | '3' -> R.drawable.ic_temp_3 40 | '4' -> R.drawable.ic_temp_4 41 | '5' -> R.drawable.ic_temp_5 42 | '6' -> R.drawable.ic_temp_6 43 | '7' -> R.drawable.ic_temp_7 44 | '8' -> R.drawable.ic_temp_8 45 | '9' -> R.drawable.ic_temp_9 46 | else -> null 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/Weather.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.data 17 | 18 | /** 19 | * The data contains basic weather info. 20 | * 21 | * @author 凛 (https://github.com/RinOrz) 22 | */ 23 | interface Weather { 24 | val temperature: Temperature 25 | val type: WeatherType 26 | 27 | /** The weather air quality index. */ 28 | val aqi: AQI 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/WeatherForecasts.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.data 17 | 18 | import com.meowbase.toolkit.formatTime 19 | import java.text.SimpleDateFormat 20 | import java.util.* 21 | 22 | /** 23 | * A city weather forecasts for the present and future. 24 | * 25 | * @author 凛 (https://github.com/RinOrz) 26 | * 27 | * @param city Represents the data comes from this city’s forecast 28 | * @param updateTime Timestamp of the last update time of the data 29 | * @param forecast All weather forecasts for today and the future 30 | * 31 | * ``` 32 | * val data: WeatherForecasts 33 | * // Fetch to tomorrow's weather forecast 34 | * data.get(1) 35 | * ``` 36 | */ 37 | data class WeatherForecasts( 38 | val city: City, 39 | val updateTime: Long, 40 | private val forecast: List 41 | ) : List by forecast { 42 | /** 43 | * Returns today's weather. 44 | */ 45 | val today: DayWeather get() = this[0] 46 | 47 | /** 48 | * Returns to all weather in the future. 49 | */ 50 | val future: List get() = this.subList(1, this.size) 51 | 52 | /** 53 | * Returns a human-readable time string. 54 | * 55 | * @param format The pattern applied to format the timestamp [WeatherForecasts.updateTime] 56 | * @param locale The locale whose date format symbols should be used 57 | * @see SimpleDateFormat 58 | */ 59 | fun readableUpdateTime( 60 | format: String = "E HH:mm:ss", 61 | locale: Locale = Locale.ENGLISH /*FIXME Localization Locale.getDefault()*/ 62 | ): String = formatTime(updateTime, pattern = format, locale = locale) 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/data/WeatherType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.data 17 | 18 | import androidx.annotation.DrawableRes 19 | import androidx.compose.ui.graphics.Color 20 | import com.example.androiddevchallenge.R 21 | 22 | /** 23 | * The info of weather type. 24 | * 25 | * @author 凛 (https://github.com/RinOrz) 26 | * 27 | * @param icon The weather type icon 28 | * @param colors Theme gradient colors for different weather 29 | */ 30 | enum class WeatherType( 31 | @DrawableRes val icon: Int, 32 | val colors: List 33 | ) { 34 | Sunny(R.mipmap.sunny, listOf(Color(0xFFE15A5A), Color(0xFFC8A471))), 35 | Cloudy(R.mipmap.cloudy, listOf(Color(0xFF67A960), Color(0xFF8E7F45))), 36 | Shower(R.mipmap.shower, listOf(Color(0xFF945AF8), Color(0xFFDE616D))), 37 | Thundershower(R.mipmap.thundershower, listOf(Color(0xFF3E6FF0), Color(0xFF35C1B2))), 38 | Snow(R.mipmap.snow, listOf(Color(0xFF3AAAD9), Color(0xFFA394C5))), 39 | Fog(R.mipmap.fog, listOf(Color(0xFFEA9827), Color(0xFFB7B382))), 40 | Sandstorm(R.mipmap.sandstorm, listOf(Color(0xFFB57B45), Color(0xFF9A8787))), 41 | Haze(R.mipmap.haze, listOf(Color(0xFF5B687A), Color(0xFFB6BBC3))), 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/di/AppSystemModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.di 17 | 18 | import android.content.Context 19 | import android.content.SharedPreferences 20 | import android.content.res.AssetManager 21 | import dagger.Module 22 | import dagger.Provides 23 | import dagger.hilt.InstallIn 24 | import dagger.hilt.android.qualifiers.ApplicationContext 25 | import dagger.hilt.components.SingletonComponent 26 | import javax.inject.Singleton 27 | 28 | /** 29 | * 注册 App 全局相关的系统级模块 30 | * 31 | * @author 凛 (https://github.com/RinOrz) 32 | */ 33 | @Module @InstallIn(SingletonComponent::class) 34 | object AppSystemModule { 35 | @Provides @Singleton 36 | fun provideAssets( 37 | @ApplicationContext context: Context 38 | ): AssetManager = context.assets 39 | 40 | /** TODO Migration to Jetpack-DataStore */ 41 | @Provides @Singleton 42 | fun provideDataStore( 43 | @ApplicationContext context: Context 44 | ): SharedPreferences = context.getSharedPreferences("global", Context.MODE_PRIVATE) 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/di/MoshiModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2021 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 | package com.example.androiddevchallenge.di 17 | 18 | import com.example.androiddevchallenge.data.CityNameAdapter 19 | import com.squareup.moshi.Moshi 20 | import dagger.Module 21 | import dagger.Provides 22 | import dagger.hilt.InstallIn 23 | import dagger.hilt.components.SingletonComponent 24 | import javax.inject.Singleton 25 | 26 | /** 27 | * 注册 Moshi 模块 28 | * 29 | * @author 凛 (https://github.com/RinOrz) 30 | */ 31 | @Module @InstallIn(SingletonComponent::class) 32 | object MoshiModule { 33 | @Provides @Singleton 34 | fun provideMoshi(): Moshi = Moshi.Builder() 35 | .add(CityNameAdapter()) 36 | .build() 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2021 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 | package com.example.androiddevchallenge.di 17 | 18 | import android.content.Context 19 | import coil.ImageLoader 20 | import coil.request.CachePolicy 21 | import dagger.Module 22 | import dagger.Provides 23 | import dagger.hilt.InstallIn 24 | import dagger.hilt.android.qualifiers.ApplicationContext 25 | import dagger.hilt.components.SingletonComponent 26 | import okhttp3.OkHttpClient 27 | import javax.inject.Singleton 28 | 29 | /** 30 | * 集中注册所有与网络相关的模块 31 | * Centrally register all modules about network. 32 | * 33 | * @author 凛 (https://github.com/RinOrz) 34 | */ 35 | @Module @InstallIn(SingletonComponent::class) 36 | object NetworkModule { 37 | 38 | @Provides @Singleton 39 | fun provideImageLoader( 40 | @ApplicationContext context: Context, 41 | okHttpClient: OkHttpClient 42 | ) = ImageLoader.Builder(context) 43 | .memoryCachePolicy(CachePolicy.ENABLED) 44 | .diskCachePolicy(CachePolicy.ENABLED) 45 | .networkCachePolicy(CachePolicy.ENABLED) 46 | .okHttpClient(okHttpClient) 47 | .build() 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/repository/CitiesRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2021 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 | package com.example.androiddevchallenge.repository 17 | 18 | import android.content.SharedPreferences 19 | import com.example.androiddevchallenge.data.City 20 | import javax.inject.Inject 21 | import javax.inject.Singleton 22 | 23 | /** 24 | * Provides cities data. 25 | * 26 | * @author 凛 (https://github.com/RinOrz) 27 | */ 28 | @Singleton 29 | class CitiesRepository @Inject constructor( 30 | private val dataStore: SharedPreferences, 31 | private val mockRepository: MockRepository 32 | ) { 33 | fun findByCode(cityCode: Int, provinceCode: Int? = null): City? = mockRepository.findByCode(cityCode, provinceCode) 34 | fun getByCode(cityCode: Int, provinceCode: Int? = null): City = findByCode(cityCode, provinceCode)!! 35 | 36 | /** 获得当前定位的城市 */ 37 | fun getLocation(): City = mockRepository.locationCity 38 | 39 | /** 获得手动添加的所有城市 */ 40 | fun getAllAdd(): Set = mockRepository.customCities 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/repository/MockRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.repository 17 | 18 | import android.content.res.AssetManager 19 | import com.example.androiddevchallenge.data.AQI 20 | import com.example.androiddevchallenge.data.City 21 | import com.example.androiddevchallenge.data.DayWeather 22 | import com.example.androiddevchallenge.data.HourlyWeather 23 | import com.example.androiddevchallenge.data.Temperature 24 | import com.example.androiddevchallenge.data.WeatherForecasts 25 | import com.example.androiddevchallenge.data.WeatherType 26 | import com.meowbase.toolkit.getNextHour 27 | import com.meowbase.toolkit.int 28 | import com.meowbase.toolkit.isToday 29 | import com.squareup.moshi.Moshi 30 | import com.squareup.moshi.adapter 31 | import okio.buffer 32 | import okio.source 33 | import java.text.SimpleDateFormat 34 | import java.util.* 35 | import javax.inject.Inject 36 | import javax.inject.Singleton 37 | import kotlin.random.Random 38 | 39 | /** 40 | * Providers all mock data to the compose-weather application. 41 | * 42 | * @author 凛 (https://github.com/RinOrz) 43 | */ 44 | @Singleton 45 | @OptIn(ExperimentalStdlibApi::class) 46 | class MockRepository @Inject constructor( 47 | moshi: Moshi, 48 | assets: AssetManager, 49 | ) { 50 | private val cities: List by lazy { 51 | val json = assets.open("cities.json").source().buffer() 52 | moshi.adapter>().fromJson(json)!! 53 | } 54 | 55 | private val cacheForecasts = hashMapOf() 56 | 57 | val locationCity by lazy { 58 | cities[cities.indices.random()] 59 | } 60 | 61 | val customCities by lazy { 62 | val result = mutableSetOf() 63 | while (result.isEmpty() || result.any { it == locationCity } || result.size < 3) { 64 | val index = cities.indices.random() 65 | result.add(cities[index]) 66 | } 67 | result 68 | } 69 | 70 | fun findByCode(cityCode: Int, provinceCode: Int?): City? = cities.find { 71 | if (provinceCode != null && it.provinceCode != provinceCode) return@find false 72 | it.code == cityCode 73 | } 74 | 75 | fun geWeatherForecasts(city: City) = cacheForecasts.getOrPut(city) { 76 | WeatherForecasts( 77 | city = city, 78 | forecast = listOf(getDayWeather()) + getFutureWeatherForecasts(), 79 | updateTime = System.currentTimeMillis(), 80 | ) 81 | } 82 | 83 | private fun getDayWeather(): DayWeather = DayWeather( 84 | aqi = getAQI(), 85 | type = getWeatherType(), 86 | temperature = getTemperature(), 87 | hours24 = getInNext24Hours(), 88 | wind = Random.nextDouble(1.0, 117.0).toFloat(), 89 | humidity = Random.nextDouble(0.0, 100.0).toFloat(), 90 | pressure = Random.nextDouble(900.0, 1100.0).toFloat(), 91 | visibility = Random.nextDouble(1.0, 30.0).toFloat(), 92 | sunrise = getTime(4..7), 93 | sunset = getTime(17..20), 94 | ) 95 | 96 | private fun getTime(hourRange: IntRange): Long { 97 | val hour = hourRange.random() 98 | val minute = (0..60).random() 99 | return SimpleDateFormat("H:m", Locale.getDefault()).parse("$hour:$minute")!!.time 100 | } 101 | 102 | private fun getFutureWeatherForecasts(): List = mutableListOf().apply { 103 | // 模拟接下来七天的天气 104 | repeat(7) { this += getDayWeather() } 105 | } 106 | 107 | private fun getInNext24Hours(): List { 108 | val hours = mutableListOf() 109 | val baseTemperature = getTemperature() 110 | 111 | // 模拟时段的预报 112 | fun getForecastInfo(time: Long): HourlyWeather { 113 | val temperatureDiff = if (time.isToday) { 114 | (0..6).random() - 3 115 | } else { 116 | (0..12).random() - 6 117 | } 118 | return HourlyWeather( 119 | aqi = getAQI(), 120 | type = getWeatherType(), 121 | // 模拟 24 小时内的细微温差 122 | temperature = baseTemperature.copy(realtime = baseTemperature.realtime + temperatureDiff), 123 | timestamp = time 124 | ) 125 | } 126 | 127 | repeat(24) { hour -> 128 | hours += getForecastInfo(time = getNextHour(hour)) 129 | } 130 | return hours 131 | } 132 | 133 | private fun getTemperature(): Temperature { 134 | val base = (100..152).random() 135 | val min = Random.nextDouble(base * 0.8, base * 0.96) 136 | val max = Random.nextDouble(base * 1.12, base * 1.15) 137 | val realtime = Random.nextDouble(min, max) 138 | return Temperature( 139 | realtime = realtime - 126, 140 | max = max - 126, 141 | min = min - 126 142 | ) 143 | } 144 | 145 | private fun getWeatherType() = when ((0..7).random()) { 146 | 0 -> WeatherType.Sunny 147 | 1 -> WeatherType.Cloudy 148 | 2 -> WeatherType.Shower 149 | 3 -> WeatherType.Thundershower 150 | 4 -> WeatherType.Snow 151 | 5 -> WeatherType.Fog 152 | 6 -> WeatherType.Sandstorm 153 | 7 -> WeatherType.Haze 154 | else -> TODO("未知的天气类型") 155 | } 156 | 157 | private fun getAQI(): AQI { 158 | val index = (0..310).random() 159 | val pm25 = ((index / 2)..index + 80).random() 160 | val pm10 = ((index / 3)..((index * 0.88).int)).random() 161 | val no2 = ((index / 6)..(index / 3)).random() 162 | val so2 = (0..(index / 10)).random() 163 | val co = (0..(index / 12)).random() 164 | val o3 = (0..index).random() 165 | 166 | return AQI(index, pm25, pm10, so2, no2, co, o3) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/repository/ProfileRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2021 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 | package com.example.androiddevchallenge.repository 17 | 18 | import javax.inject.Inject 19 | import javax.inject.Singleton 20 | 21 | /** 22 | * 提供个人资料屏幕所需要的数据 23 | * Provides data to profile screen. 24 | * 25 | * @author 凛 (https://github.com/RinOrz) 26 | */ 27 | @Singleton 28 | class ProfileRepository @Inject constructor() { 29 | 30 | /** 31 | * 获取个人头像 32 | * Get personal avatar. 33 | * 34 | * TODO: Get data from DataStore. 35 | */ 36 | fun getAvatar(): Any = "https://avatars.githubusercontent.com/u/58068445?s=460&u=87b1e18b75533c3ec88b1f5f34b33499d0e0897c&v=4" 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/repository/WeatherRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2021 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 | package com.example.androiddevchallenge.repository 17 | 18 | import com.example.androiddevchallenge.data.City 19 | import com.example.androiddevchallenge.data.WeatherForecasts 20 | import javax.inject.Inject 21 | import javax.inject.Singleton 22 | 23 | /** 24 | * Provides weather data. 25 | * 26 | * @author 凛 (https://github.com/RinOrz) 27 | */ 28 | @Singleton 29 | class WeatherRepository @Inject constructor( 30 | citiesRepository: CitiesRepository, 31 | private val mockRepository: MockRepository, 32 | ) { 33 | private val currentCity = citiesRepository.getLocation() 34 | 35 | /** 根据定位获取天气预报 */ 36 | fun getByLocation() = getByCity(currentCity) 37 | 38 | /** 根据城市获取天气预报 */ 39 | fun getByCity(city: City): WeatherForecasts = mockRepository.geWeatherForecasts(city) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/WeatherApp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021-2021 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 | package com.example.androiddevchallenge.ui 17 | 18 | import androidx.compose.foundation.background 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.foundation.layout.fillMaxSize 21 | import androidx.compose.material.LocalContentColor 22 | import androidx.compose.material.MaterialTheme 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.CompositionLocalProvider 25 | import androidx.compose.runtime.remember 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.unit.dp 28 | import androidx.hilt.navigation.compose.hiltNavGraphViewModel 29 | import androidx.navigation.NavType 30 | import androidx.navigation.compose.NavHost 31 | import androidx.navigation.compose.composable 32 | import androidx.navigation.compose.navArgument 33 | import androidx.navigation.compose.rememberNavController 34 | import com.example.androiddevchallenge.data.LocalPadding 35 | import com.example.androiddevchallenge.data.Padding 36 | import com.example.androiddevchallenge.data.Screen 37 | import com.example.androiddevchallenge.ui.details.weather.WeatherDetails 38 | import com.example.androiddevchallenge.ui.home.Home 39 | import dev.chrisbanes.accompanist.insets.ProvideWindowInsets 40 | 41 | /** 42 | * Weather App 的 UI 唯一入口点 43 | * The only UI entry point for the Weather App. 44 | * 45 | * @author 凛 (https://github.com/RinOrz) 46 | */ 47 | @Composable 48 | fun WeatherApp() { 49 | val navController = rememberNavController() 50 | 51 | ProvideWindowInsets { 52 | CompositionLocalProvider( 53 | LocalContentColor provides MaterialTheme.colors.onBackground, 54 | LocalPadding provides remember { Padding(24.dp, 24.dp) }, 55 | ) { 56 | Box( 57 | modifier = Modifier 58 | .fillMaxSize() 59 | .background(MaterialTheme.colors.background) 60 | ) { 61 | NavHost(navController, startDestination = Screen.Home.route) { 62 | composable(Screen.Home.route) { 63 | Home( 64 | navController = navController, 65 | viewModel = hiltNavGraphViewModel() 66 | ) 67 | } 68 | composable( 69 | Screen.Details.Weather.route, 70 | arguments = listOf( 71 | navArgument("cityCode") { type = NavType.IntType }, 72 | navArgument("provinceCode") { type = NavType.IntType }, 73 | ) 74 | ) { 75 | // TODO: A more elegant way? 76 | WeatherDetails( 77 | navController = navController, 78 | viewModel = hiltNavGraphViewModel(), 79 | cityCode = it.arguments!!.getInt("cityCode"), 80 | provinceCode = it.arguments!!.getInt("provinceCode") 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/AnimateAsState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | 18 | import androidx.compose.animation.animateColorAsState 19 | import androidx.compose.animation.core.AnimationConstants 20 | import androidx.compose.animation.core.AnimationSpec 21 | import androidx.compose.animation.core.Easing 22 | import androidx.compose.animation.core.FastOutSlowInEasing 23 | import androidx.compose.animation.core.Spring 24 | import androidx.compose.animation.core.VisibilityThreshold 25 | import androidx.compose.animation.core.animateDpAsState 26 | import androidx.compose.animation.core.animateFloatAsState 27 | import androidx.compose.animation.core.animateIntAsState 28 | import androidx.compose.animation.core.animateIntOffsetAsState 29 | import androidx.compose.animation.core.animateIntSizeAsState 30 | import androidx.compose.animation.core.animateOffsetAsState 31 | import androidx.compose.animation.core.animateRectAsState 32 | import androidx.compose.animation.core.animateSizeAsState 33 | import androidx.compose.animation.core.spring 34 | import androidx.compose.animation.core.tween 35 | import androidx.compose.runtime.Composable 36 | import androidx.compose.runtime.State 37 | import androidx.compose.ui.geometry.Offset 38 | import androidx.compose.ui.geometry.Rect 39 | import androidx.compose.ui.geometry.Size 40 | import androidx.compose.ui.graphics.Color 41 | import androidx.compose.ui.unit.Dp 42 | import androidx.compose.ui.unit.IntOffset 43 | import androidx.compose.ui.unit.IntSize 44 | 45 | @Suppress("UNCHECKED_CAST") 46 | @Composable 47 | fun animateAsState( 48 | targetValue: T, 49 | durationMillis: Int = AnimationConstants.DefaultDurationMillis, 50 | delayMillis: Int = 0, 51 | easing: Easing = FastOutSlowInEasing, 52 | finishedListener: ((T) -> Unit)? = null 53 | ): State = when (targetValue) { 54 | is Dp -> animateDpAsState(targetValue, tween(durationMillis, delayMillis, easing), finishedListener as? (Dp) -> Unit) 55 | is Float -> animateFloatAsState(targetValue, tween(durationMillis, delayMillis, easing), finishedListener = finishedListener as? (Float) -> Unit) 56 | is Color -> animateColorAsState(targetValue, tween(durationMillis, delayMillis, easing), finishedListener as? (Color) -> Unit) 57 | is Int -> animateIntAsState(targetValue, tween(durationMillis, delayMillis, easing), finishedListener as? (Int) -> Unit) 58 | is Offset -> animateOffsetAsState(targetValue, tween(durationMillis, delayMillis, easing), finishedListener as? (Offset) -> Unit) 59 | is IntOffset -> animateIntOffsetAsState(targetValue, tween(durationMillis, delayMillis, easing), finishedListener as? (IntOffset) -> Unit) 60 | is IntSize -> animateIntSizeAsState(targetValue, tween(durationMillis, delayMillis, easing), finishedListener as? (IntSize) -> Unit) 61 | is Rect -> animateRectAsState(targetValue, tween(durationMillis, delayMillis, easing), finishedListener as? (Rect) -> Unit) 62 | is Size -> animateSizeAsState(targetValue, tween(durationMillis, delayMillis, easing), finishedListener as? (Size) -> Unit) 63 | else -> TODO("Support to ${targetValue::class.java.name}") 64 | } as State 65 | 66 | @Suppress("UNCHECKED_CAST") 67 | @Composable 68 | fun animateAsState( 69 | targetValue: T, 70 | visibilityThreshold: T, 71 | dampingRatio: Float = Spring.DampingRatioNoBouncy, 72 | stiffness: Float = Spring.StiffnessMedium, 73 | finishedListener: ((T) -> Unit)? = null 74 | ): State = when (targetValue) { 75 | is Dp -> animateDpAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold) as AnimationSpec, finishedListener as? (Dp) -> Unit) 76 | is Float -> animateFloatAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold) as AnimationSpec, finishedListener = finishedListener as? (Float) -> Unit) 77 | is Color -> animateColorAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold) as AnimationSpec, finishedListener as? (Color) -> Unit) 78 | is Int -> animateIntAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold) as AnimationSpec, finishedListener as? (Int) -> Unit) 79 | is Offset -> animateOffsetAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold) as AnimationSpec, finishedListener as? (Offset) -> Unit) 80 | is IntOffset -> animateIntOffsetAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold) as AnimationSpec, finishedListener as? (IntOffset) -> Unit) 81 | is IntSize -> animateIntSizeAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold) as AnimationSpec, finishedListener as? (IntSize) -> Unit) 82 | is Rect -> animateRectAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold) as AnimationSpec, finishedListener as? (Rect) -> Unit) 83 | is Size -> animateSizeAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold) as AnimationSpec, finishedListener as? (Size) -> Unit) 84 | else -> TODO("Support to ${targetValue::class.java.name}") 85 | } as State 86 | 87 | @Suppress("UNCHECKED_CAST") 88 | @Composable 89 | fun animateAsState( 90 | targetValue: T, 91 | dampingRatio: Float = Spring.DampingRatioNoBouncy, 92 | stiffness: Float = Spring.StiffnessMedium, 93 | finishedListener: ((T) -> Unit)? = null 94 | ): State = when (targetValue) { 95 | is Dp -> animateDpAsState(targetValue, spring(dampingRatio, stiffness, Dp.VisibilityThreshold), finishedListener as? (Dp) -> Unit) 96 | is Float -> animateFloatAsState(targetValue, spring(dampingRatio, stiffness, visibilityThreshold = 0.01f), finishedListener = finishedListener as? (Float) -> Unit) 97 | is Color -> animateColorAsState(targetValue, spring(dampingRatio, stiffness, null), finishedListener as? (Color) -> Unit) 98 | is Int -> animateIntAsState(targetValue, spring(dampingRatio, stiffness, Int.VisibilityThreshold), finishedListener as? (Int) -> Unit) 99 | is Offset -> animateOffsetAsState(targetValue, spring(dampingRatio, stiffness, Offset.VisibilityThreshold), finishedListener as? (Offset) -> Unit) 100 | is IntOffset -> animateIntOffsetAsState(targetValue, spring(dampingRatio, stiffness, IntOffset.VisibilityThreshold), finishedListener as? (IntOffset) -> Unit) 101 | is IntSize -> animateIntSizeAsState(targetValue, spring(dampingRatio, stiffness, IntSize.VisibilityThreshold), finishedListener as? (IntSize) -> Unit) 102 | is Rect -> animateRectAsState(targetValue, spring(dampingRatio, stiffness, Rect.VisibilityThreshold), finishedListener as? (Rect) -> Unit) 103 | is Size -> animateSizeAsState(targetValue, spring(dampingRatio, stiffness, Size.VisibilityThreshold), finishedListener as? (Size) -> Unit) 104 | else -> TODO("Support to ${targetValue::class.java.name}") 105 | } as State 106 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/AnimationSpec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/Button.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | 18 | import androidx.compose.foundation.BorderStroke 19 | import androidx.compose.foundation.interaction.MutableInteractionSource 20 | import androidx.compose.foundation.layout.PaddingValues 21 | import androidx.compose.foundation.layout.RowScope 22 | import androidx.compose.material.Button 23 | import androidx.compose.material.ButtonColors 24 | import androidx.compose.material.ButtonDefaults 25 | import androidx.compose.material.MaterialTheme 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Shape 30 | import androidx.compose.ui.unit.Dp 31 | 32 | @Composable 33 | fun FlatButton( 34 | onClick: () -> Unit, 35 | modifier: Modifier = Modifier, 36 | enabled: Boolean = true, 37 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 38 | shape: Shape = MaterialTheme.shapes.small, 39 | border: BorderStroke? = null, 40 | colors: ButtonColors = ButtonDefaults.buttonColors(), 41 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 42 | content: @Composable RowScope.() -> Unit 43 | ) { 44 | Button( 45 | onClick, 46 | modifier, 47 | enabled, 48 | interactionSource, 49 | ButtonDefaults.elevation(Dp.Unspecified, Dp.Unspecified, Dp.Unspecified), 50 | shape, 51 | border, 52 | colors, 53 | contentPadding, 54 | content 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/Clickable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | 18 | import androidx.compose.foundation.Indication 19 | import androidx.compose.foundation.IndicationInstance 20 | import androidx.compose.foundation.LocalIndication 21 | import androidx.compose.foundation.clickable 22 | import androidx.compose.foundation.interaction.Interaction 23 | import androidx.compose.foundation.interaction.InteractionSource 24 | import androidx.compose.foundation.interaction.MutableInteractionSource 25 | import androidx.compose.material.ripple.rememberRipple 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.composed 30 | import androidx.compose.ui.graphics.Color 31 | import androidx.compose.ui.semantics.Role 32 | import androidx.compose.ui.unit.Dp 33 | import kotlinx.coroutines.flow.Flow 34 | 35 | fun Modifier.rippleClickable( 36 | bounded: Boolean = true, 37 | radius: Dp = Dp.Unspecified, 38 | color: Color = Color.Unspecified, 39 | enabled: Boolean = true, 40 | onClickLabel: String? = null, 41 | role: Role? = null, 42 | onClick: () -> Unit 43 | ) = composed { 44 | clickable( 45 | interactionSource = remember { MutableInteractionSource() }, 46 | indication = rememberRipple(bounded, radius, color), 47 | enabled = enabled, 48 | onClickLabel = onClickLabel, 49 | role = role, 50 | onClick = onClick 51 | ) 52 | } 53 | 54 | fun Modifier.clickable( 55 | enabled: Boolean = true, 56 | onClickLabel: String? = null, 57 | role: Role? = null, 58 | interactionSource: MutableInteractionSource = DefaultMutableInteractionSource, 59 | indication: Indication? = DefaultIndication, 60 | onClick: () -> Unit 61 | ) = composed { 62 | val realIndication = if (indication === DefaultIndication) { 63 | LocalIndication.current 64 | } else { 65 | indication 66 | } 67 | val realInteractionSource = if (interactionSource === DefaultMutableInteractionSource) { 68 | remember { MutableInteractionSource() } 69 | } else { 70 | interactionSource 71 | } 72 | 73 | clickable(realInteractionSource, realIndication, enabled, onClickLabel, role, onClick) 74 | } 75 | 76 | private object DefaultIndication : Indication { 77 | @Composable 78 | override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { 79 | TODO("Not yet implemented") 80 | } 81 | } 82 | 83 | private object DefaultMutableInteractionSource : MutableInteractionSource { 84 | override val interactions: Flow 85 | get() = TODO("Not yet implemented") 86 | 87 | override suspend fun emit(interaction: Interaction) { 88 | TODO("Not yet implemented") 89 | } 90 | 91 | override fun tryEmit(interaction: Interaction): Boolean { 92 | TODO("Not yet implemented") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/Icon.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | 18 | import androidx.annotation.DrawableRes 19 | import androidx.compose.material.LocalContentAlpha 20 | import androidx.compose.material.LocalContentColor 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.res.painterResource 25 | 26 | @Composable 27 | fun Icon( 28 | @DrawableRes id: Int, 29 | modifier: Modifier = Modifier, 30 | contentDescription: String? = null, 31 | tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) 32 | ) { 33 | androidx.compose.material.Icon( 34 | painter = painterResource(id), 35 | contentDescription, modifier, tint 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/Image.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | 18 | import androidx.annotation.DrawableRes 19 | import androidx.compose.foundation.layout.BoxScope 20 | import androidx.compose.foundation.layout.sizeIn 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.ColorFilter 25 | import androidx.compose.ui.graphics.DefaultAlpha 26 | import androidx.compose.ui.layout.ContentScale 27 | import androidx.compose.ui.res.painterResource 28 | import androidx.compose.ui.unit.IntSize 29 | import androidx.compose.ui.unit.dp 30 | import coil.ImageLoader 31 | import coil.request.ImageRequest 32 | import dev.chrisbanes.accompanist.coil.CoilImageDefaults 33 | import dev.chrisbanes.accompanist.imageloading.DefaultRefetchOnSizeChangeLambda 34 | import dev.chrisbanes.accompanist.imageloading.EmptyRequestCompleteLambda 35 | import dev.chrisbanes.accompanist.imageloading.ImageLoadState 36 | 37 | @Composable 38 | fun Image( 39 | @DrawableRes id: Int, 40 | modifier: Modifier = Modifier, 41 | contentDescription: String? = null, 42 | alignment: Alignment = Alignment.Center, 43 | contentScale: ContentScale = ContentScale.Fit, 44 | alpha: Float = DefaultAlpha, 45 | colorFilter: ColorFilter? = null 46 | ) { 47 | androidx.compose.foundation.Image( 48 | painter = painterResource(id), 49 | contentDescription, modifier, alignment, contentScale, alpha, colorFilter 50 | ) 51 | } 52 | 53 | @Composable 54 | fun CoilImage( 55 | data: Any, 56 | modifier: Modifier = Modifier, 57 | contentDescription: String? = null, 58 | alignment: Alignment = Alignment.Center, 59 | contentScale: ContentScale = ContentScale.Fit, 60 | colorFilter: ColorFilter? = null, 61 | fadeIn: Boolean = false, 62 | requestBuilder: (ImageRequest.Builder.(size: IntSize) -> ImageRequest.Builder)? = null, 63 | imageLoader: ImageLoader = CoilImageDefaults.defaultImageLoader(), 64 | shouldRefetchOnSizeChange: (currentResult: ImageLoadState, size: IntSize) -> Boolean = DefaultRefetchOnSizeChangeLambda, 65 | onRequestCompleted: (ImageLoadState) -> Unit = EmptyRequestCompleteLambda, 66 | error: @Composable (BoxScope.(ImageLoadState.Error) -> Unit)? = null, 67 | loading: @Composable (BoxScope.() -> Unit)? = null, 68 | ) { 69 | dev.chrisbanes.accompanist.coil.CoilImage( 70 | data, 71 | contentDescription, 72 | modifier.sizeIn(minWidth = 1.dp, minHeight = 1.dp), 73 | alignment, 74 | contentScale, 75 | colorFilter, 76 | fadeIn, 77 | requestBuilder, 78 | imageLoader, 79 | shouldRefetchOnSizeChange, 80 | onRequestCompleted, 81 | error, 82 | loading 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/LinearProgressIndicator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | 18 | import androidx.compose.foundation.background 19 | import androidx.compose.foundation.focusable 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.BoxWithConstraints 22 | import androidx.compose.foundation.layout.fillMaxWidth 23 | import androidx.compose.foundation.layout.height 24 | import androidx.compose.foundation.progressSemantics 25 | import androidx.compose.material.LocalContentColor 26 | import androidx.compose.material.ProgressIndicatorDefaults 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.graphics.Brush 31 | import androidx.compose.ui.graphics.Color 32 | import androidx.compose.ui.unit.dp 33 | 34 | private val LinearIndicatorHeight = ProgressIndicatorDefaults.StrokeWidth 35 | private val LinearIndicatorWidth = 240.dp 36 | 37 | @Composable 38 | fun LinearProgressIndicator( 39 | progress: Float, 40 | brush: Brush, 41 | modifier: Modifier = Modifier, 42 | backgroundColor: Color = LocalContentColor.current.copy(alpha = ProgressIndicatorDefaults.IndicatorBackgroundOpacity) 43 | ) { 44 | BoxWithConstraints( 45 | modifier 46 | .background(backgroundColor) 47 | .progressSemantics(progress) 48 | .focusable(), 49 | contentAlignment = Alignment.BottomCenter 50 | ) { 51 | val barStart = maxHeight * (1f - progress) 52 | Box(modifier = Modifier.height(barStart).fillMaxWidth().background(brush)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/RightTrapezoid.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | 18 | import androidx.compose.foundation.Canvas 19 | import androidx.compose.material.LocalContentColor 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.geometry.Size 24 | import androidx.compose.ui.graphics.Brush 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.graphics.Outline 27 | import androidx.compose.ui.graphics.Paint 28 | import androidx.compose.ui.graphics.Path 29 | import androidx.compose.ui.graphics.PathEffect 30 | import androidx.compose.ui.graphics.Shape 31 | import androidx.compose.ui.graphics.isSpecified 32 | import androidx.compose.ui.platform.LocalDensity 33 | import androidx.compose.ui.platform.LocalLayoutDirection 34 | import androidx.compose.ui.unit.Density 35 | import androidx.compose.ui.unit.Dp 36 | import androidx.compose.ui.unit.LayoutDirection 37 | import androidx.compose.ui.unit.isSpecified 38 | import com.meowbase.toolkit.toRadians 39 | import kotlin.math.tan 40 | 41 | /** 42 | * 圆角梯形 43 | * TODO: Move to separate library module. 44 | * 45 | * @author 凛 (https://github.com/RinOrz) 46 | */ 47 | @Composable 48 | fun RoundedTrapezoid( 49 | angle: Float, 50 | brush: Brush, 51 | modifier: Modifier = Modifier, 52 | cornerRadius: Dp = Dp.Unspecified, 53 | alpha: Float = 1f, 54 | using: TrapezoidShape.Corner = TrapezoidShape.Corner.BottomEnd, 55 | ) { GenericRoundedTrapezoid(angle, brush, Color.Unspecified, modifier, cornerRadius, alpha, using) } 56 | 57 | /** 58 | * 圆角梯形 59 | * TODO: Move to separate library module. 60 | * 61 | * @author 凛 (https://github.com/RinOrz) 62 | */ 63 | @Composable 64 | fun RoundedTrapezoid( 65 | angle: Float, 66 | modifier: Modifier = Modifier, 67 | cornerRadius: Dp = Dp.Unspecified, 68 | color: Color = LocalContentColor.current, 69 | alpha: Float = 1f, 70 | using: TrapezoidShape.Corner = TrapezoidShape.Corner.BottomEnd, 71 | ) { GenericRoundedTrapezoid(angle, null, color, modifier, cornerRadius, alpha, using) } 72 | 73 | /** 74 | * 圆角梯形 75 | * TODO: Move to separate library module. 76 | * 77 | * @author 凛 (https://github.com/RinOrz) 78 | */ 79 | @Composable 80 | private fun GenericRoundedTrapezoid( 81 | angle: Float, 82 | brush: Brush?, 83 | color: Color, 84 | modifier: Modifier = Modifier, 85 | cornerRadius: Dp = Dp.Unspecified, 86 | alpha: Float = 1f, 87 | using: TrapezoidShape.Corner = TrapezoidShape.Corner.BottomEnd, 88 | ) { 89 | val density = LocalDensity.current 90 | val layoutDirection = LocalLayoutDirection.current 91 | val shape = remember(angle, using) { TrapezoidShape(angle, using) } 92 | val radius = remember(cornerRadius) { with(density) { cornerRadius.toPx() } } 93 | Canvas(modifier) { 94 | val outline = shape.createOutline(size, layoutDirection, density) as Outline.Generic 95 | val paint = Paint().apply { 96 | if (color.isSpecified) this.color = color 97 | if (cornerRadius.isSpecified) this.pathEffect = PathEffect.cornerPathEffect(radius) 98 | } 99 | brush?.applyTo(size, paint, alpha) 100 | drawContext.canvas.drawPath(outline.path, paint) 101 | } 102 | } 103 | 104 | /** 105 | * 直角梯形 106 | * TODO: Move to separate library module. 107 | * 108 | * @author 凛 (https://github.com/RinOrz) 109 | */ 110 | data class TrapezoidShape internal constructor( 111 | val angle: Float, 112 | val using: Corner = Corner.BottomEnd 113 | ) : Shape { 114 | override fun createOutline( 115 | size: Size, 116 | layoutDirection: LayoutDirection, 117 | density: Density 118 | ): Outline { 119 | val anotherHeight = size.width * tan(angle.toRadians()) 120 | val path = Path().apply { 121 | when (using) { 122 | Corner.TopStart -> TODO() 123 | Corner.TopEnd -> TODO() 124 | Corner.BottomEnd -> { 125 | lineTo(size.width, 0f) 126 | lineTo(size.width, size.height - anotherHeight) 127 | lineTo(0f, size.height) 128 | } 129 | Corner.BottomStart -> TODO() 130 | } 131 | close() 132 | } 133 | return Outline.Generic(path) 134 | } 135 | 136 | enum class Corner { 137 | TopStart, 138 | TopEnd, 139 | BottomEnd, 140 | BottomStart, 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/Shape.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | 18 | import androidx.compose.foundation.Canvas 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.size 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.graphics.Shape 26 | import androidx.compose.ui.unit.Dp 27 | import com.example.androiddevchallenge.ui.theme.currentShapes 28 | 29 | @Composable 30 | fun Circle(size: Dp, color: Color, modifier: Modifier = Modifier) { 31 | Canvas(modifier = modifier.size(size)) { drawCircle(color) } 32 | } 33 | 34 | @Composable 35 | fun Shape(color: Color, modifier: Modifier = Modifier, shape: Shape = currentShapes.large) { 36 | Box(modifier = modifier.background(color, shape)) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/composable/Text.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.composable 17 | 18 | import androidx.compose.material.LocalTextStyle 19 | import androidx.compose.material.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.text.TextLayoutResult 24 | import androidx.compose.ui.text.TextStyle 25 | import androidx.compose.ui.text.font.FontStyle 26 | import androidx.compose.ui.text.font.FontWeight 27 | import androidx.compose.ui.text.style.TextAlign 28 | import androidx.compose.ui.text.style.TextDecoration 29 | import androidx.compose.ui.text.style.TextOverflow 30 | import androidx.compose.ui.unit.TextUnit 31 | import com.example.androiddevchallenge.ui.theme.WeatherFont 32 | 33 | /** 34 | * 纯数字文本 35 | * 36 | * @author 凛 (https://github.com/RinOrz) 37 | */ 38 | @Composable 39 | fun NumberText( 40 | text: String, 41 | modifier: Modifier = Modifier, 42 | color: Color = Color.Unspecified, 43 | fontSize: TextUnit = TextUnit.Unspecified, 44 | fontStyle: FontStyle? = null, 45 | fontWeight: FontWeight? = FontWeight.Normal, 46 | letterSpacing: TextUnit = TextUnit.Unspecified, 47 | textDecoration: TextDecoration? = null, 48 | textAlign: TextAlign? = null, 49 | lineHeight: TextUnit = TextUnit.Unspecified, 50 | overflow: TextOverflow = TextOverflow.Clip, 51 | softWrap: Boolean = true, 52 | maxLines: Int = Int.MAX_VALUE, 53 | onTextLayout: (TextLayoutResult) -> Unit = {}, 54 | style: TextStyle = LocalTextStyle.current 55 | ) { 56 | Text( 57 | text, 58 | modifier, 59 | color, 60 | fontSize, 61 | fontStyle, 62 | fontWeight, 63 | WeatherFont.Rubik, 64 | letterSpacing, 65 | textDecoration, 66 | textAlign, 67 | lineHeight, 68 | overflow, 69 | softWrap, 70 | maxLines, 71 | onTextLayout, 72 | style 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/details/weather/Details.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.details.weather 17 | 18 | import androidx.compose.foundation.ExperimentalFoundationApi 19 | import androidx.compose.foundation.layout.Column 20 | import androidx.compose.foundation.layout.Spacer 21 | import androidx.compose.foundation.layout.fillMaxHeight 22 | import androidx.compose.foundation.layout.height 23 | import androidx.compose.foundation.rememberScrollState 24 | import androidx.compose.foundation.verticalScroll 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.CompositionLocalProvider 27 | import androidx.compose.runtime.compositionLocalOf 28 | import androidx.compose.runtime.getValue 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.unit.dp 31 | import androidx.navigation.NavHostController 32 | import com.example.androiddevchallenge.R 33 | import dev.chrisbanes.accompanist.insets.navigationBarsPadding 34 | import dev.chrisbanes.accompanist.insets.statusBarsPadding 35 | import kotlin.math.roundToInt 36 | 37 | /** 38 | * The only entry point to the HomePage UI. 39 | * 40 | * @author 凛 (https://github.com/RinOrz) 41 | */ 42 | @OptIn(ExperimentalFoundationApi::class) 43 | @Composable 44 | fun WeatherDetails( 45 | navController: NavHostController, 46 | viewModel: ViewModel, 47 | cityCode: Int, 48 | provinceCode: Int 49 | ) { 50 | val state by viewModel.state 51 | val forecast = viewModel.geWeatherForecasts(cityCode, provinceCode) 52 | val weather = forecast.today 53 | CompositionLocalProvider(LocalWeatherDetailsState provides state) { 54 | Column( 55 | modifier = Modifier 56 | .statusBarsPadding() 57 | .navigationBarsPadding() 58 | .fillMaxHeight() 59 | .verticalScroll(rememberScrollState()) 60 | ) { 61 | Board( 62 | temperature = weather.temperature.realtime, 63 | title = weather.type.name, 64 | subtitle = forecast.city.name, 65 | trailingText = weather.aqi.value.toString(), 66 | colors = weather.type.colors, 67 | icon = weather.type.icon, 68 | onTrailingButtonClick = { /*TODO*/ } 69 | ) 70 | Spacer(modifier = Modifier.height(12.dp)) 71 | GridRow { 72 | GridItem( 73 | icon = R.drawable.ic_humidity, 74 | subtitle = "Humidity", 75 | title = weather.humidity.roundToInt().toString(), 76 | unit = "%", 77 | progress = weather.humidity, 78 | maxProgress = 100f, 79 | progressColors = weather.type.colors 80 | ) 81 | GridItem( 82 | icon = R.drawable.ic_visibility, 83 | subtitle = "Visibility", 84 | title = weather.visibility.roundToInt().toString(), 85 | unit = "km", 86 | progress = weather.visibility, 87 | maxProgress = 40f, 88 | progressColors = weather.type.colors 89 | ) 90 | } 91 | Spacer(modifier = Modifier.height(18.dp)) 92 | GridRow { 93 | GridItem( 94 | icon = R.drawable.ic_wind, 95 | subtitle = "Wind", 96 | title = weather.wind.roundToInt().toString(), 97 | unit = "km/h", 98 | progress = weather.wind, 99 | maxProgress = 150f, 100 | progressColors = weather.type.colors 101 | ) 102 | GridItem( 103 | icon = R.drawable.ic_pressure, 104 | subtitle = "Pressure", 105 | title = weather.pressure.roundToInt().toString(), 106 | unit = "hPa", 107 | progress = weather.pressure, 108 | maxProgress = 1100f, 109 | progressColors = weather.type.colors 110 | ) 111 | } 112 | // TODO 113 | // SunriseSunset() 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * The [WeatherDetails] will display different UI 120 | * according to this state. 121 | * 122 | * @see DetailsTransition 123 | */ 124 | enum class DetailsState { 125 | Expand, 126 | Collapse; 127 | 128 | val isExpand get() = this == Expand 129 | val isCollapse get() = this == Collapse 130 | } 131 | 132 | val LocalWeatherDetailsState = compositionLocalOf { DetailsState.Expand } 133 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/details/weather/GridItem.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.details.weather 17 | 18 | import androidx.compose.foundation.ExperimentalFoundationApi 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Arrangement 21 | import androidx.compose.foundation.layout.Column 22 | import androidx.compose.foundation.layout.Row 23 | import androidx.compose.foundation.layout.RowScope 24 | import androidx.compose.foundation.layout.aspectRatio 25 | import androidx.compose.foundation.layout.fillMaxHeight 26 | import androidx.compose.foundation.layout.fillMaxWidth 27 | import androidx.compose.foundation.layout.padding 28 | import androidx.compose.foundation.layout.size 29 | import androidx.compose.foundation.layout.width 30 | import androidx.compose.material.LocalContentColor 31 | import androidx.compose.material.Text 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.ui.Alignment 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.draw.clip 36 | import androidx.compose.ui.graphics.Brush 37 | import androidx.compose.ui.graphics.Color 38 | import androidx.compose.ui.text.font.FontWeight 39 | import androidx.compose.ui.unit.dp 40 | import androidx.compose.ui.unit.sp 41 | import com.example.androiddevchallenge.data.LocalPadding 42 | import com.example.androiddevchallenge.ui.composable.Icon 43 | import com.example.androiddevchallenge.ui.composable.LinearProgressIndicator 44 | import com.example.androiddevchallenge.ui.theme.currentShapes 45 | 46 | /** 47 | * Grid row in details. 48 | * 49 | * @author 凛 (https://github.com/RinOrz) 50 | */ 51 | @Composable 52 | fun GridRow(content: @Composable RowScope.() -> Unit) { 53 | Row( 54 | modifier = Modifier.fillMaxWidth().padding(horizontal = LocalPadding.current.horizontal), 55 | horizontalArrangement = Arrangement.spacedBy(18.dp), 56 | content = content 57 | ) 58 | } 59 | 60 | /** 61 | * Grid item in details. 62 | */ 63 | @OptIn(ExperimentalFoundationApi::class) 64 | @Composable 65 | fun RowScope.GridItem( 66 | icon: Int, 67 | title: String, 68 | subtitle: String, 69 | unit: String, 70 | progress: Float, 71 | maxProgress: Float, 72 | progressColors: List 73 | ) { 74 | val contentColor = LocalContentColor.current 75 | Row( 76 | modifier = Modifier.weight(1f).background( 77 | brush = Brush.verticalGradient( 78 | listOf( 79 | contentColor.copy(alpha = 0.04f), 80 | contentColor.copy(alpha = 0.01f) 81 | ) 82 | ), 83 | shape = currentShapes.small 84 | ).aspectRatio(1f) 85 | ) { 86 | Column( 87 | modifier = Modifier 88 | .padding( 89 | vertical = LocalPadding.current.vertical, 90 | horizontal = LocalPadding.current.horizontal 91 | ) 92 | .weight(1f) 93 | ) { 94 | Icon(id = icon, modifier = Modifier.size(36.dp)) 95 | Text( 96 | text = subtitle, 97 | color = LocalContentColor.current.copy(alpha = 0.4f), 98 | fontWeight = FontWeight.SemiBold, 99 | fontSize = 14.sp, 100 | modifier = Modifier.padding(top = 18.dp) 101 | ) 102 | Row( 103 | modifier = Modifier.padding(top = 4.dp), 104 | verticalAlignment = Alignment.Bottom 105 | ) { 106 | Text( 107 | text = title, 108 | fontWeight = FontWeight.Bold, 109 | fontSize = 26.sp, 110 | ) 111 | // Text( 112 | // text = " $unit", 113 | // fontWeight = FontWeight.SemiBold, 114 | // fontSize = 18.sp, 115 | // modifier = Modifier.padding(bottom = 4.dp) 116 | // ) 117 | } 118 | } 119 | 120 | LinearProgressIndicator( 121 | progress / maxProgress, 122 | brush = Brush.verticalGradient(progressColors), 123 | modifier = Modifier 124 | .padding(end = LocalPadding.current.horizontal) 125 | .padding(vertical = 34.dp) 126 | .fillMaxHeight() 127 | .width(6.dp) 128 | .clip(currentShapes.large) 129 | ) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/details/weather/Transition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.details.weather 17 | 18 | import androidx.compose.animation.core.Transition 19 | import androidx.compose.animation.core.animateDp 20 | import androidx.compose.animation.core.animateFloat 21 | import androidx.compose.animation.core.tween 22 | import androidx.compose.animation.core.updateTransition 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.State 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.ui.unit.Dp 28 | import androidx.compose.ui.unit.dp 29 | 30 | /** 31 | * 提供在不同 [DetailsState] 下 32 | * 显示的 UI 所需的动画值 33 | * 34 | * 目前以硬编码形式将过渡总时长限制在 600 ms 内 35 | * 36 | * @author 凛 (https://github.com/RinOrz) 37 | */ 38 | data class DetailsTransition( 39 | val board: Board, 40 | ) { 41 | 42 | class Board(alpha: State, offsetY: State) { 43 | val alpha by alpha 44 | val offsetY by offsetY 45 | } 46 | 47 | companion object { 48 | const val MaxDuration = 500L 49 | } 50 | } 51 | 52 | /** 53 | * 创建并记住 [DetailsState], 54 | * 以根据不同的主页 [state] 来返回预期的动画值 55 | */ 56 | @Composable 57 | fun rememberDetailsTransition(state: DetailsState): DetailsTransition = 58 | updateTransition(state, label = "HomeTransition").run { 59 | val toolbar = rememberBoard() 60 | remember(this) { 61 | DetailsTransition(toolbar) 62 | } 63 | } 64 | 65 | /** @see [Board] */ 66 | @Composable 67 | private fun DetailsStateTransition.rememberBoard(): DetailsTransition.Board { 68 | val alpha = animateFloat({ tween(400, delayMillis = 20) }) { state -> 69 | if (state.isExpand) 1f else 0f 70 | } 71 | val offsetY = animateDp({ tween(500, delayMillis = 20) }) { state -> 72 | if (state.isExpand) 0.dp else 100.dp 73 | } 74 | return remember(this) { DetailsTransition.Board(alpha, offsetY) } 75 | } 76 | 77 | private typealias DetailsStateTransition = Transition 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/details/weather/ViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | @file:Suppress("CanBeParameter") 17 | 18 | package com.example.androiddevchallenge.ui.details.weather 19 | 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.lifecycle.ViewModel 22 | import com.example.androiddevchallenge.repository.CitiesRepository 23 | import com.example.androiddevchallenge.repository.WeatherRepository 24 | import dagger.hilt.android.lifecycle.HiltViewModel 25 | import javax.inject.Inject 26 | 27 | /** 28 | * The ViewModel of weather details screen. 29 | * 30 | * @author 凛 (https://github.com/RinOrz) 31 | */ 32 | @HiltViewModel 33 | class ViewModel @Inject constructor( 34 | private val weatherRepository: WeatherRepository, 35 | private val citiesRepository: CitiesRepository, 36 | ) : ViewModel() { 37 | val state = mutableStateOf(DetailsState.Expand) 38 | 39 | fun geWeatherForecasts(cityCode: Int, provinceCode: Int) = 40 | weatherRepository.getByCity(citiesRepository.getByCode(cityCode, provinceCode)) 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/home/ForecastsBar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.home 17 | 18 | import androidx.compose.foundation.background 19 | import androidx.compose.foundation.layout.Column 20 | import androidx.compose.foundation.layout.PaddingValues 21 | import androidx.compose.foundation.layout.Row 22 | import androidx.compose.foundation.layout.Spacer 23 | import androidx.compose.foundation.layout.fillMaxWidth 24 | import androidx.compose.foundation.layout.height 25 | import androidx.compose.foundation.layout.offset 26 | import androidx.compose.foundation.layout.padding 27 | import androidx.compose.foundation.layout.size 28 | import androidx.compose.foundation.lazy.LazyRow 29 | import androidx.compose.foundation.lazy.items 30 | import androidx.compose.material.ButtonDefaults.textButtonColors 31 | import androidx.compose.material.LocalContentColor 32 | import androidx.compose.material.Text 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.runtime.getValue 35 | import androidx.compose.ui.Alignment 36 | import androidx.compose.ui.Modifier 37 | import androidx.compose.ui.draw.alpha 38 | import androidx.compose.ui.graphics.Brush 39 | import androidx.compose.ui.graphics.Color 40 | import androidx.compose.ui.text.font.FontWeight 41 | import androidx.compose.ui.unit.dp 42 | import androidx.compose.ui.unit.sp 43 | import com.example.androiddevchallenge.R 44 | import com.example.androiddevchallenge.data.HourlyWeather 45 | import com.example.androiddevchallenge.data.LocalPadding 46 | import com.example.androiddevchallenge.ui.composable.CoilImage 47 | import com.example.androiddevchallenge.ui.composable.FlatButton 48 | import com.example.androiddevchallenge.ui.composable.Icon 49 | import com.example.androiddevchallenge.ui.composable.NumberText 50 | import com.example.androiddevchallenge.ui.composable.animateAsState 51 | import com.example.androiddevchallenge.ui.theme.currentShapes 52 | import com.example.androiddevchallenge.ui.theme.currentTypography 53 | import com.meowbase.toolkit.getNextHour 54 | import com.meowbase.toolkit.isInTime 55 | import com.meowbase.toolkit.toCalendar 56 | import java.util.* 57 | import kotlin.math.roundToInt 58 | 59 | /** 60 | * Show the future hourly forecast. 61 | * 62 | * @author 凛 (https://github.com/RinOrz) 63 | */ 64 | @Composable 65 | fun ForecastsBar( 66 | hourlyForecasts: List, 67 | forecastDays: Int, 68 | highlightColors: List 69 | ) { 70 | val transition = rememberHomeTransition(state = LocalHomeState.current) 71 | Column( 72 | modifier = Modifier 73 | .fillMaxWidth() 74 | .alpha(transition.forecastsBar.alpha) 75 | .offset(y = transition.forecastsBar.offsetY) 76 | ) { 77 | Days(forecastDays) 78 | Times(hourlyForecasts, highlightColors) 79 | } 80 | } 81 | 82 | @Composable 83 | private fun Days(forecastDays: Int) { 84 | Row( 85 | modifier = Modifier.padding(start = LocalPadding.current.horizontal, end = 12.dp), 86 | verticalAlignment = Alignment.CenterVertically 87 | ) { 88 | Text(text = "Hourly Forecast", style = currentTypography.h6) 89 | Spacer(modifier = Modifier.weight(1f)) 90 | FlatButton( 91 | onClick = { /*TODO*/ }, 92 | colors = textButtonColors(contentColor = LocalContentColor.current.copy(alpha = 0.28f)), 93 | // 箭头图标自带稍微的边距,所以这里可以缩小右侧边距 94 | contentPadding = PaddingValues(start = 16.dp, end = 12.dp) 95 | ) { 96 | Text( 97 | text = "Next ${forecastDays - 1} Days", 98 | fontSize = 14.sp, 99 | style = currentTypography.caption 100 | ) 101 | Icon( 102 | id = R.drawable.ic_baseline_navigate_next_24, 103 | modifier = Modifier 104 | .padding(start = 4.dp) 105 | .size(18.dp) 106 | ) 107 | } 108 | } 109 | } 110 | 111 | @Composable 112 | private fun Times( 113 | hourlyForecasts: List, 114 | highlightColors: List 115 | ) { 116 | val gradientStart by animateAsState(targetValue = highlightColors[0]) 117 | val gradientEnd by animateAsState(targetValue = highlightColors[1]) 118 | LazyRow( 119 | contentPadding = PaddingValues( 120 | start = 8.dp, 121 | end = LocalPadding.current.horizontal, 122 | top = LocalPadding.current.vertical, 123 | bottom = LocalPadding.current.vertical, 124 | ) 125 | ) { 126 | items(hourlyForecasts) { weather -> 127 | val beginHour = Date(weather.timestamp) 128 | val endHour = Date(getNextHour(1, beginHour.toCalendar())) 129 | // 当前时间是否在预测时段内 130 | val isInPeriod = isInTime(beginHour, endHour) 131 | val background = if (isInPeriod) { 132 | Modifier.background( 133 | brush = Brush.verticalGradient(listOf(gradientStart, gradientEnd)), 134 | shape = currentShapes.large 135 | ) 136 | } else { 137 | Modifier.background( 138 | color = LocalContentColor.current.copy(0.04f), 139 | shape = currentShapes.large 140 | ) 141 | } 142 | 143 | Column( 144 | horizontalAlignment = Alignment.CenterHorizontally, 145 | modifier = Modifier 146 | .padding(start = 16.dp) 147 | .then(background) 148 | .padding(16.dp) 149 | ) { 150 | if (isInPeriod) { 151 | Text( 152 | text = "Now", 153 | fontWeight = FontWeight.SemiBold, 154 | style = currentTypography.body2, 155 | modifier = Modifier.padding(top = 6.dp), 156 | ) 157 | } else { 158 | NumberText( 159 | text = "${weather.hour}:00", 160 | style = currentTypography.body2, 161 | modifier = Modifier.padding(top = 6.dp), 162 | ) 163 | } 164 | CoilImage( 165 | data = weather.type.icon, 166 | fadeIn = true, 167 | modifier = Modifier.size(54.dp) 168 | ) 169 | Spacer(modifier = Modifier.height(8.dp)) 170 | NumberText( 171 | text = "${weather.temperature.realtime.roundToInt()}°", 172 | style = currentTypography.body1 173 | ) 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/home/Home.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.home 17 | 18 | import androidx.compose.foundation.layout.Column 19 | import androidx.compose.foundation.layout.padding 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.CompositionLocalProvider 22 | import androidx.compose.runtime.LaunchedEffect 23 | import androidx.compose.runtime.compositionLocalOf 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.ui.Modifier 26 | import androidx.navigation.NavHostController 27 | import com.example.androiddevchallenge.data.LocalPadding 28 | import com.example.androiddevchallenge.data.Screen 29 | import com.example.androiddevchallenge.data.navigate 30 | import dev.chrisbanes.accompanist.insets.navigationBarsPadding 31 | import dev.chrisbanes.accompanist.insets.statusBarsPadding 32 | import kotlinx.coroutines.delay 33 | 34 | /** 35 | * The only entry point to the HomePage UI. 36 | * 37 | * @author 凛 (https://github.com/RinOrz) 38 | */ 39 | @Composable 40 | fun Home(navController: NavHostController, viewModel: ViewModel) { 41 | val state by viewModel.state 42 | val avatar by viewModel.avatar 43 | val allCityForecasts = viewModel.allCityForecasts 44 | val currentCityPage by viewModel.currentCityPage 45 | val todayForecast = viewModel.currentCityForecast 46 | val todayWeather = todayForecast.today 47 | 48 | CompositionLocalProvider(LocalHomeState provides state) { 49 | Column( 50 | modifier = Modifier 51 | .statusBarsPadding() 52 | .navigationBarsPadding() 53 | ) { 54 | TopBar( 55 | avatar = avatar, 56 | title = todayForecast.city.name, 57 | subtitle = "Updated on ${todayForecast.readableUpdateTime()}", 58 | trailingText = todayWeather.aqi.value.toString(), 59 | onMenuClick = { /*TODO*/ }, 60 | onSearchClick = { /*TODO*/ }, 61 | onTrailingButtonClick = { /*TODO*/ }, 62 | isPositioned = currentCityPage == 0, 63 | ) 64 | 65 | Pager( 66 | modifier = Modifier 67 | .weight(1f) 68 | .padding(vertical = LocalPadding.current.vertical), 69 | currentCityPage = currentCityPage, 70 | onPageClick = viewModel::showDetails, 71 | onPageChanged = viewModel::changeCityPage, 72 | allCityForecasts = allCityForecasts, 73 | ) 74 | ForecastsBar( 75 | hourlyForecasts = todayWeather.hours24, 76 | forecastDays = todayForecast.size, 77 | highlightColors = todayWeather.type.colors, 78 | ) 79 | } 80 | } 81 | 82 | LaunchedEffect(state) { 83 | if (state.isExpandDetails) { 84 | delay(HomeTransition.MaxDuration) 85 | navController.navigate( 86 | Screen.Details.Weather, 87 | "cityCode" to todayForecast.city.code, 88 | "provinceCode" to todayForecast.city.provinceCode 89 | ) 90 | viewModel.hideDetails() 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * The [Home] will display different UI 97 | * according to this state. 98 | * 99 | * @see HomeTransition 100 | */ 101 | enum class HomeState { 102 | Initially, 103 | ExpandDetails; 104 | 105 | val isInitial get() = this == Initially 106 | val isExpandDetails get() = this == ExpandDetails 107 | } 108 | 109 | val LocalHomeState = compositionLocalOf { HomeState.Initially } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/home/TopBar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.home 17 | 18 | import androidx.compose.foundation.layout.Column 19 | import androidx.compose.foundation.layout.PaddingValues 20 | import androidx.compose.foundation.layout.Row 21 | import androidx.compose.foundation.layout.Spacer 22 | import androidx.compose.foundation.layout.fillMaxWidth 23 | import androidx.compose.foundation.layout.offset 24 | import androidx.compose.foundation.layout.padding 25 | import androidx.compose.foundation.layout.size 26 | import androidx.compose.foundation.layout.width 27 | import androidx.compose.foundation.shape.CircleShape 28 | import androidx.compose.material.ButtonDefaults.buttonColors 29 | import androidx.compose.material.IconButton 30 | import androidx.compose.material.LocalContentColor 31 | import androidx.compose.material.Text 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.ui.Alignment 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.draw.alpha 36 | import androidx.compose.ui.draw.clip 37 | import androidx.compose.ui.text.style.TextOverflow 38 | import androidx.compose.ui.unit.dp 39 | import com.example.androiddevchallenge.R 40 | import com.example.androiddevchallenge.data.LocalPadding 41 | import com.example.androiddevchallenge.ui.composable.CoilImage 42 | import com.example.androiddevchallenge.ui.composable.FlatButton 43 | import com.example.androiddevchallenge.ui.composable.Icon 44 | import com.example.androiddevchallenge.ui.composable.NumberText 45 | import com.example.androiddevchallenge.ui.theme.currentTypography 46 | 47 | /** 48 | * The separate TopBar of HomePage. 49 | * 50 | * @author 凛 (https://github.com/RinOrz) 51 | */ 52 | @Composable 53 | fun TopBar( 54 | title: String, 55 | subtitle: String, 56 | avatar: Any, 57 | trailingText: String, 58 | onMenuClick: () -> Unit, 59 | onSearchClick: () -> Unit, 60 | onTrailingButtonClick: () -> Unit, 61 | isPositioned: Boolean, 62 | ) { 63 | ToolBar(avatar, onMenuClick, onSearchClick) 64 | TitleBar(title, subtitle, trailingText, onTrailingButtonClick, isPositioned) 65 | } 66 | 67 | @Composable 68 | private fun ToolBar( 69 | avatar: Any, 70 | onMenuClick: () -> Unit, 71 | onSearchClick: () -> Unit, 72 | ) { 73 | val transition = rememberHomeTransition(state = LocalHomeState.current) 74 | Row( 75 | modifier = Modifier 76 | .fillMaxWidth() 77 | .padding( 78 | start = 14.dp, 79 | top = 12.dp, 80 | bottom = 16.dp, 81 | end = LocalPadding.current.horizontal, 82 | ) 83 | .alpha(transition.toolBar.alpha) 84 | .offset(y = transition.toolBar.offsetY), 85 | verticalAlignment = Alignment.CenterVertically 86 | ) { 87 | IconButton(onClick = onMenuClick) { 88 | Icon( 89 | id = R.drawable.ic_menu, 90 | modifier = Modifier.size(26.dp) 91 | ) 92 | } 93 | Spacer(modifier = Modifier.weight(1f)) 94 | IconButton(onClick = onSearchClick) { 95 | Icon( 96 | id = R.drawable.ic_search, 97 | modifier = Modifier.size(20.dp) 98 | ) 99 | } 100 | CoilImage( 101 | data = avatar, 102 | modifier = Modifier 103 | .padding(start = 14.dp) 104 | .size(38.dp) 105 | .clip(CircleShape) 106 | ) 107 | } 108 | } 109 | 110 | @Composable 111 | private fun TitleBar( 112 | title: String, 113 | subtitle: String, 114 | trailingText: String, 115 | onTrailingButtonClick: () -> Unit, 116 | isPositioned: Boolean 117 | ) { 118 | val transition = rememberHomeTransition(state = LocalHomeState.current) 119 | val color = LocalContentColor.current 120 | Row( 121 | modifier = Modifier 122 | .padding(horizontal = LocalPadding.current.horizontal) 123 | .alpha(transition.titleBar.alpha), 124 | verticalAlignment = Alignment.CenterVertically 125 | ) { 126 | Column(modifier = Modifier.weight(1f)) { 127 | Row(verticalAlignment = Alignment.CenterVertically) { 128 | Text( 129 | text = title, 130 | color = color, 131 | maxLines = 1, 132 | overflow = TextOverflow.Ellipsis, 133 | style = currentTypography.h5, 134 | ) 135 | if (isPositioned) CoilImage( 136 | data = R.mipmap.ic_location, 137 | modifier = Modifier 138 | .padding(start = 8.dp) 139 | .size(18.dp) 140 | ) 141 | } 142 | Text( 143 | text = subtitle, 144 | maxLines = 1, 145 | color = color.copy(0.18f), 146 | overflow = TextOverflow.Ellipsis, 147 | style = currentTypography.subtitle2 148 | ) 149 | } 150 | Spacer(modifier = Modifier.width(LocalPadding.current.horizontal)) 151 | FlatButton( 152 | onClick = onTrailingButtonClick, 153 | colors = buttonColors( 154 | backgroundColor = color.copy(0.04f), 155 | contentColor = color 156 | ), 157 | contentPadding = PaddingValues( 158 | horizontal = 16.dp, 159 | vertical = 12.dp 160 | ) 161 | ) { 162 | Icon(id = R.drawable.ic_aqi, modifier = Modifier.size(20.dp)) 163 | Spacer(modifier = Modifier.width(8.dp)) 164 | NumberText(text = trailingText) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/home/Transition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.home 17 | 18 | import androidx.compose.animation.core.Transition 19 | import androidx.compose.animation.core.animateDp 20 | import androidx.compose.animation.core.animateFloat 21 | import androidx.compose.animation.core.tween 22 | import androidx.compose.animation.core.updateTransition 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.State 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.ui.unit.Dp 28 | import androidx.compose.ui.unit.dp 29 | 30 | /** 31 | * 提供在不同 [HomeState] 下 32 | * 显示的 UI 所需的动画值 33 | * 34 | * 目前以硬编码形式将过渡总时长限制在 600 ms 内 35 | * 36 | * @author 凛 (https://github.com/RinOrz) 37 | */ 38 | data class HomeTransition( 39 | val toolBar: ToolBar, 40 | val titleBar: TitleBar, 41 | val pager: Pager, 42 | val forecastsBar: ForecastsBar, 43 | ) { 44 | 45 | class TitleBar(alpha: State) { 46 | val alpha by alpha 47 | } 48 | 49 | class ToolBar(alpha: State, offsetY: State) { 50 | val alpha by alpha 51 | val offsetY by offsetY 52 | } 53 | 54 | class ForecastsBar(alpha: State, offsetY: State) { 55 | val alpha by alpha 56 | val offsetY by offsetY 57 | } 58 | 59 | class Pager( 60 | currentBounce: State, 61 | currentFade: State, 62 | otherClose: State, 63 | weatherOffset: State, 64 | angle: State 65 | ) { 66 | val currentBounce by currentBounce 67 | val currentFade by currentFade 68 | val otherFade by otherClose 69 | val weatherOffset by weatherOffset 70 | val angle by angle 71 | } 72 | 73 | companion object { 74 | const val MaxDuration = 460L 75 | } 76 | } 77 | 78 | /** 79 | * 创建并记住 [HomeTransition], 80 | * 以根据不同的主页 [state] 来返回预期的动画值 81 | */ 82 | @Composable 83 | fun rememberHomeTransition(state: HomeState): HomeTransition = 84 | updateTransition(state, label = "HomeTransition").run { 85 | val toolbar = rememberToolBar() 86 | val titleBar = rememberTitleBar() 87 | val pager = rememberPager() 88 | val forecastsBar = rememberForecastsBar() 89 | remember(this) { 90 | HomeTransition(toolbar, titleBar, pager, forecastsBar) 91 | } 92 | } 93 | 94 | /** @see [ToolBar] */ 95 | @Composable 96 | private fun HomeStateTransition.rememberToolBar(): HomeTransition.ToolBar { 97 | val alpha = animateFloat({ tween(400) }) { state -> 98 | if (state.isExpandDetails) 0f else 1f 99 | } 100 | val offsetY = animateDp({ tween(560) }) { state -> 101 | if (state.isExpandDetails) (-50).dp else 0.dp 102 | } 103 | return remember(this) { HomeTransition.ToolBar(alpha, offsetY) } 104 | } 105 | 106 | /** @see [TitleBar] */ 107 | @Composable 108 | private fun HomeStateTransition.rememberTitleBar(): HomeTransition.TitleBar { 109 | val alpha = animateFloat({ tween() }) { state -> 110 | if (state.isExpandDetails) 0f else 1f 111 | } 112 | return remember(this) { HomeTransition.TitleBar(alpha) } 113 | } 114 | 115 | /** @see [ForecastsBar] */ 116 | @Composable 117 | private fun HomeStateTransition.rememberForecastsBar(): HomeTransition.ForecastsBar { 118 | val alpha = animateFloat({ tween(500) }) { state -> 119 | if (state.isExpandDetails) 0f else 1f 120 | } 121 | val offsetY = animateDp({ tween(500) }) { state -> 122 | if (state.isExpandDetails) 80.dp else 0.dp 123 | } 124 | return remember(this) { HomeTransition.ForecastsBar(alpha, offsetY) } 125 | } 126 | 127 | /** @see [ForecastsBar] */ 128 | @Composable 129 | private fun HomeStateTransition.rememberPager(): HomeTransition.Pager { 130 | val currentBounce = animateDp({ tween(420) }) { state -> 131 | if (state.isExpandDetails) 20.dp else 0.dp 132 | } 133 | val currentFade = animateFloat({ tween(400, delayMillis = 200) }) { state -> 134 | if (state.isExpandDetails) 0.2f else 1f 135 | } 136 | val weatherOffset = animateDp({ tween(400, delayMillis = 200) }) { state -> 137 | if (state.isExpandDetails) 50.dp else 0.dp 138 | } 139 | val otherClose = animateFloat({ tween(200) }) { state -> 140 | if (state.isExpandDetails) 0f else 1f 141 | } 142 | val angle = animateFloat({ tween(300) }) { state -> 143 | if (state.isExpandDetails) 0f else 5f 144 | } 145 | return remember(this) { HomeTransition.Pager(currentBounce, currentFade, otherClose, weatherOffset, angle) } 146 | } 147 | 148 | private typealias HomeStateTransition = Transition 149 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/home/ViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | @file:Suppress("CanBeParameter") 17 | 18 | package com.example.androiddevchallenge.ui.home 19 | 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.toMutableStateList 22 | import androidx.lifecycle.ViewModel 23 | import com.example.androiddevchallenge.data.WeatherForecasts 24 | import com.example.androiddevchallenge.repository.CitiesRepository 25 | import com.example.androiddevchallenge.repository.ProfileRepository 26 | import com.example.androiddevchallenge.repository.WeatherRepository 27 | import dagger.hilt.android.lifecycle.HiltViewModel 28 | import javax.inject.Inject 29 | 30 | /** 31 | * 用于管理主页屏幕的数据 32 | * The ViewModel of home screen. 33 | * 34 | * @author 凛 (https://github.com/RinOrz) 35 | */ 36 | @HiltViewModel 37 | class ViewModel @Inject constructor( 38 | private val weatherRepository: WeatherRepository, 39 | private val citiesRepository: CitiesRepository, 40 | private val profileRepository: ProfileRepository, 41 | ) : ViewModel() { 42 | 43 | /** Weather forecasts for all cities. */ 44 | val allCityForecasts = listOf(weatherRepository.getByLocation()) 45 | .plus(citiesRepository.getAllAdd().map(weatherRepository::getByCity)) 46 | .toMutableStateList() 47 | 48 | /** The profile picture */ 49 | val avatar = mutableStateOf(profileRepository.getAvatar()) 50 | 51 | /** Represents the current [Home] state, related to the navigation transition. */ 52 | val state = mutableStateOf(HomeState.Initially) 53 | 54 | /** Weather page of the current city in the [Home] pager. */ 55 | val currentCityPage = mutableStateOf(0) 56 | 57 | /** Weather forecasts of the current city in the [Home] pager. */ 58 | val currentCityForecast: WeatherForecasts get() = allCityForecasts[currentCityPage.value] 59 | 60 | /** Change the city page index of [Home] pager. */ 61 | fun changeCityPage(index: Int) { 62 | currentCityPage.value = index 63 | } 64 | 65 | /** 66 | * 显示当前城市天气的详细信息 67 | */ 68 | fun showDetails() { 69 | state.value = HomeState.ExpandDetails 70 | } 71 | 72 | /** 73 | * 隐藏天气详情以回到主页 74 | */ 75 | fun hideDetails() { 76 | state.value = HomeState.Initially 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | @file:Suppress("unused") 17 | 18 | package com.example.androiddevchallenge.ui.theme 19 | 20 | import androidx.compose.material.Colors 21 | import androidx.compose.material.MaterialTheme 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.graphics.Color 24 | 25 | /** 26 | * All theme colors of compose-weather application. 27 | * 28 | * @author 凛 (https://github.com/RinOrz) 29 | */ 30 | val Colors.blue get() = Color(0xFF4452FB) 31 | 32 | /** 33 | * All gradient colors of compose-weather application. 34 | */ 35 | object GradientColors { 36 | val textStart 37 | @Composable 38 | get() = if (MaterialTheme.colors.isLight) { 39 | Color(0xFF34374D) 40 | } else { 41 | Color.White 42 | } 43 | 44 | val textEnd 45 | @Composable 46 | get() = textStart.copy(alpha = 0.04f) 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.theme 17 | 18 | import androidx.compose.foundation.shape.RoundedCornerShape 19 | import androidx.compose.material.Shapes 20 | import androidx.compose.ui.unit.dp 21 | 22 | val shapes = Shapes( 23 | small = RoundedCornerShape(size = 22.dp), 24 | large = RoundedCornerShape(percent = 50) 25 | ) 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.theme 17 | 18 | import androidx.compose.foundation.isSystemInDarkTheme 19 | import androidx.compose.material.MaterialTheme 20 | import androidx.compose.material.darkColors 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.graphics.Color 23 | 24 | private val DarkColorPalette = darkColors( 25 | background = Color(0xFF111111), 26 | onBackground = Color.White, 27 | surface = Color.White.copy(0.025f), 28 | onSurface = Color.White, 29 | ) 30 | 31 | @Composable 32 | fun WeatherTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 33 | // TODO Light Theme 34 | val colors = DarkColorPalette 35 | // val colors = if (darkTheme) { 36 | // DarkColorPalette 37 | // } else { 38 | // LightColorPalette 39 | // } 40 | 41 | MaterialTheme( 42 | colors = colors, 43 | typography = typography, 44 | shapes = shapes, 45 | content = content 46 | ) 47 | } 48 | 49 | /** 50 | * The custom theme data of compose-weather application. 51 | * 52 | * @author 凛 (https://github.com/RinOrz) 53 | */ 54 | 55 | val currentTypography 56 | @Composable 57 | get() = MaterialTheme.typography 58 | 59 | val currentShapes 60 | @Composable 61 | get() = MaterialTheme.shapes 62 | 63 | val currentColors 64 | @Composable 65 | get() = MaterialTheme.colors 66 | 67 | val currentGradientColors = GradientColors 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/ui/theme/Typography.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.ui.theme 17 | 18 | import androidx.compose.material.Typography 19 | import androidx.compose.ui.text.TextStyle 20 | import androidx.compose.ui.text.font.Font 21 | import androidx.compose.ui.text.font.FontFamily 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.sp 24 | import com.example.androiddevchallenge.R 25 | 26 | val typography = Typography( 27 | defaultFontFamily = WeatherFont.Nunito, 28 | h1 = TextStyle( 29 | fontWeight = FontWeight.Bold, 30 | fontSize = 160.sp, 31 | ), 32 | h2 = TextStyle( 33 | fontWeight = FontWeight.Bold, 34 | fontSize = 110.sp, 35 | ), 36 | h5 = TextStyle( 37 | fontWeight = FontWeight.SemiBold, 38 | fontSize = 30.sp, 39 | letterSpacing = 1.11.sp, 40 | ), 41 | h6 = TextStyle( 42 | fontWeight = FontWeight.SemiBold, 43 | fontSize = 20.sp, 44 | letterSpacing = (-0.1).sp, 45 | ), 46 | subtitle1 = TextStyle( 47 | fontSize = 15.sp, 48 | letterSpacing = 0.2.sp, 49 | ), 50 | subtitle2 = TextStyle( 51 | fontWeight = FontWeight.Bold, 52 | fontSize = 12.sp, 53 | letterSpacing = (-0.2).sp, 54 | ), 55 | body1 = TextStyle( 56 | fontWeight = FontWeight.Light, 57 | fontSize = 16.sp, 58 | ), 59 | body2 = TextStyle( 60 | fontWeight = FontWeight.Light, 61 | fontSize = 13.sp, 62 | ), 63 | caption = TextStyle( 64 | fontWeight = FontWeight.SemiBold, 65 | fontSize = 16.sp, 66 | ), 67 | button = TextStyle( 68 | fontWeight = FontWeight.Bold, 69 | fontSize = 13.sp, 70 | letterSpacing = 1.25.sp, 71 | ), 72 | ) 73 | 74 | object WeatherFont { 75 | val Rubik = FontFamily( 76 | Font(R.font.rubik_regular), 77 | Font(R.font.rubik_light, FontWeight.Light), 78 | ) 79 | 80 | val Nunito = FontFamily( 81 | Font(R.font.nunito_regular), 82 | Font(R.font.nunito_bold, FontWeight.Bold), 83 | Font(R.font.nunito_semibold, FontWeight.SemiBold), 84 | Font(R.font.nunito_extrabold, FontWeight.ExtraBold), 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/util/FontFamily.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.util 17 | 18 | import android.graphics.Typeface 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.platform.LocalFontLoader 21 | import androidx.compose.ui.text.font.FontFamily 22 | import androidx.compose.ui.text.font.FontListFontFamily 23 | import androidx.compose.ui.text.font.FontWeight 24 | 25 | @Composable 26 | fun FontFamily.toTypeface(weight: FontWeight = FontWeight.Normal): Typeface? { 27 | val font = (this as? FontListFontFamily)?.fonts?.find { it.weight == weight } 28 | return font?.let { LocalFontLoader.current.load(it) } as? Typeface 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/util/LiveData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.util 17 | 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.State 20 | import androidx.compose.runtime.livedata.observeAsState 21 | import androidx.lifecycle.LiveData 22 | 23 | @Composable 24 | fun LiveData.observeAsNonNullState(): State = observeAsState(value!!) 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/androiddevchallenge/util/Math.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 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 | package com.example.androiddevchallenge.util 17 | 18 | import androidx.compose.ui.geometry.Offset 19 | import androidx.compose.ui.geometry.Rect 20 | import androidx.compose.ui.geometry.RoundRect 21 | import androidx.compose.ui.geometry.Size 22 | import androidx.compose.ui.geometry.lerp 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.graphics.lerp 25 | import androidx.compose.ui.unit.Dp 26 | import androidx.compose.ui.unit.IntRect 27 | import androidx.compose.ui.unit.lerp 28 | import androidx.compose.ui.util.lerp 29 | 30 | @Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY") 31 | fun lerp( 32 | start: T, 33 | stop: T, 34 | fraction: Float, 35 | ): T = when { 36 | start is Dp && stop is Dp -> lerp(start, stop, fraction) 37 | start is Float && stop is Float -> lerp(start, stop, fraction) 38 | start is Int && stop is Int -> lerp(start, stop, fraction) 39 | start is Long && stop is Long -> lerp(start, stop, fraction) 40 | start is Color && stop is Color -> lerp(start, stop, fraction) 41 | start is IntRect && stop is IntRect -> lerp(start, stop, fraction) 42 | start is RoundRect && stop is RoundRect -> lerp(start, stop, fraction) 43 | start is Rect && stop is Rect -> lerp(start, stop, fraction) 44 | start is Offset && stop is Offset -> lerp(start, stop, fraction) 45 | start is Size && stop is Size -> lerp(start, stop, fraction) 46 | else -> TODO("Support to ${start::class.java.name} and ${stop::class.java.name}") 47 | } as T 48 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 11 | 17 | 18 | 19 | 25 | 28 | 31 | 32 | 33 | 34 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_aqi.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_navigate_next_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_humidity.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 17 | 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 | 175 | 180 | 181 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 16 | 20 | 24 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pressure.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sun.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 28 | 29 | 35 | 38 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_0.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_1.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_2.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_3.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_4.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_5.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_6.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_7.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_8.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_9.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_negative.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_temp_unit.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_visibility.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_wind.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/font/nunito_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_extrabold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/font/nunito_extrabold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/font/nunito_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/font/nunito_semibold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/rubik_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/font/rubik_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/rubik_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/font/rubik_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/cloudy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/cloudy.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/fog.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/fog.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/haze.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/haze.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_location.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/ic_location.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/sandstorm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/sandstorm.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/shower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/shower.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/snow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/snow.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/sunny.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/sunny.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/thundershower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-hdpi/thundershower.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/cloudy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/cloudy.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/fog.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/fog.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/haze.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/haze.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_location.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/ic_location.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/sandstorm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/sandstorm.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/shower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/shower.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/snow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/snow.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/sunny.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/sunny.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/thundershower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-mdpi/thundershower.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/cloudy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/cloudy.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/fog.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/fog.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/haze.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/haze.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_location.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/ic_location.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/sandstorm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/sandstorm.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/shower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/shower.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/snow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/snow.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/sunny.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/sunny.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/thundershower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xhdpi/thundershower.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/cloudy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/cloudy.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/fog.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/fog.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/haze.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/haze.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_location.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/ic_location.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/sandstorm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/sandstorm.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/shower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/shower.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/snow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/snow.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/sunny.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/sunny.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/thundershower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxhdpi/thundershower.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/cloudy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/cloudy.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/fog.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/fog.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/haze.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/haze.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_location.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/ic_location.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/sandstorm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/sandstorm.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/shower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/shower.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/snow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/snow.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/sunny.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/sunny.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/thundershower.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/app/src/main/res/mipmap-xxxhdpi/thundershower.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | #FFBB86FC 14 | #FF6200EE 15 | #FF3700B3 16 | #FF03DAC5 17 | #FF018786 18 | #FF000000 19 | #FFFFFFFF 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | AndroidDevChallenge 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 23 | 24 |