├── .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 | 
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 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/androiddevchallenge/ExampleUnitTest.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 | /**
19 | * Example local unit test, which will execute on the development machine (host).
20 | *
21 | * See [testing documentation](http://d.android.com/tools/testing).
22 | */
23 | class ExampleUnitTest {
24 | // Add unit tests here
25 | }
26 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | buildscript {
18 | ext.kotlin_version = '1.4.31'
19 | ext.compose_version = '1.0.0-beta02'
20 | ext.coroutines_version = '1.4.2'
21 | ext.google_hilt = "2.31.2-alpha"
22 | ext.accompanist_version = "0.6.2"
23 | ext.lifecycle_version = "2.3.0-beta01"
24 |
25 | repositories {
26 | google()
27 | jcenter()
28 | }
29 |
30 | dependencies {
31 | classpath 'com.android.tools.build:gradle:7.0.0-alpha11'
32 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
33 | classpath "com.google.dagger:hilt-android-gradle-plugin:$google_hilt"
34 | }
35 | }
36 |
37 | plugins {
38 | id 'com.diffplug.spotless' version '5.7.0'
39 | }
40 |
41 | subprojects {
42 | repositories {
43 | google()
44 | jcenter()
45 | maven { url = 'https://dl.bintray.com/oh-rin/meowbase' }
46 | }
47 |
48 | apply plugin: 'com.diffplug.spotless'
49 | spotless {
50 | kotlin {
51 | target '**/*.kt'
52 | targetExclude("$buildDir/**/*.kt")
53 | targetExclude('bin/**/*.kt')
54 |
55 | ktlint("0.40.0").userData(["disabled_rules": "no-wildcard-imports"])
56 | licenseHeaderFile rootProject.file('spotless/copyright.kt')
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/debug.keystore
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Feb 24 18:05:51 CET 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/icx.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/results/screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/results/screenshot_1.png
--------------------------------------------------------------------------------
/results/screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/results/screenshot_2.png
--------------------------------------------------------------------------------
/results/screenshot_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/results/screenshot_3.png
--------------------------------------------------------------------------------
/results/screenshot_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/results/screenshot_4.png
--------------------------------------------------------------------------------
/results/video.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chachako/compose-weather/bb1ac181bc859e6ed831d216c6955e5a1dd402e0/results/video.mp4
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = "AndroidDevChallenge"
2 | include ':app'
3 |
--------------------------------------------------------------------------------
/spotless/copyright.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright $YEAR 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 |
--------------------------------------------------------------------------------