diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml new file mode 100644 index 000000000..0171c48bb --- /dev/null +++ b/.github/blunderbuss.yml @@ -0,0 +1,3 @@ +assign_issues: + - android/compose-devrel + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..0d08e261a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/snippet-bot.yml b/.github/snippet-bot.yml new file mode 100644 index 000000000..4cbd7b2f3 --- /dev/null +++ b/.github/snippet-bot.yml @@ -0,0 +1 @@ +alwaysCreateStatusCheck: true diff --git a/.github/workflows/apply_spotless.yml b/.github/workflows/apply_spotless.yml new file mode 100644 index 000000000..3727da63a --- /dev/null +++ b/.github/workflows/apply_spotless.yml @@ -0,0 +1,50 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +name: Apply spotless + +on: + pull_request: + branches: [ '*' ] + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + apply-spotless: + runs-on: ubuntu-latest + timeout-minutes: 30 + if: github.repository == 'android/snippets' + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT || github.token }} + fetch-depth: 0 + + - name: set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Run spotlessApply + run: ./gradlew spotlessApply --stacktrace + + - name: Auto-commit if spotlessApply has changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: Apply Spotless \ No newline at end of file diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml new file mode 100644 index 000000000..6e6d9a06a --- /dev/null +++ b/.github/workflows/build-ios.yml @@ -0,0 +1,51 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +name: Build snippets + +on: + push: + branches: [ '*' ] + paths: + - 'kmp/**' + - '.github/workflows/build-ios.yml' + pull_request: + branches: [ '*' ] + paths: + - 'kmp/**' + - '.github/workflows/build-ios.yml' + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-build-ios + cancel-in-progress: true +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build_ios: + name: Build iOS app + runs-on: macos-latest + steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Checkout + uses: actions/checkout@v5 + + - name: Build iOS app + uses: mxcl/xcodebuild@v3 + with: + xcode: ^16 + scheme: iosApp + platform: iOS + action: build + working-directory: kmp/iosApp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..1d3c201ca --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +name: Build snippets + +on: + push: + branches: [ '*' ] + pull_request: + branches: [ '*' ] + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT || github.token }} + - name: set up Java 25 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '25' + - name: Build All + run: ./gradlew build --stacktrace + - name: Build Watch Face Push validation snippets + run: ./gradlew :watchfacepush:validator:run --stacktrace diff --git a/.github/workflows/sync_main_latest.yml b/.github/workflows/sync_main_latest.yml new file mode 100644 index 000000000..38f6c335b --- /dev/null +++ b/.github/workflows/sync_main_latest.yml @@ -0,0 +1,35 @@ +name: Sync main and latest +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + sync-branches: + runs-on: ubuntu-latest + name: Syncing branches + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set git config user + run: git config user.email "compose-devrel-github-bot@google.com" && git config user.name "compose-devrel-github-bot" + + - name: Merge main into latest + run: git fetch && git switch latest && git merge -s ours origin/main --allow-unrelated-histories + + - name: Create pull request + id: cpr + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.PAT }} + commit-message: 🤖 Sync main to latest + committer: compose-devrel-github-bot + author: compose-devrel-github-bot + signoff: false + branch: bot-sync-main + delete-branch: true + title: '🤖 Sync main to latest' + body: 'Update `latest` with `main`' + reviewers: ${{ github.actor }} diff --git a/.github/workflows/update_deps.yml b/.github/workflows/update_deps.yml new file mode 100644 index 000000000..566cd63e9 --- /dev/null +++ b/.github/workflows/update_deps.yml @@ -0,0 +1,33 @@ +name: Update Versions / Dependencies + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'zulu' + cache: gradle + + - name: Update dependencies + run: ./gradlew versionCatalogUpdate + - name: Create pull request + id: cpr + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.PAT }} + commit-message: 🤖 Update Dependencies + committer: compose-devrel-github-bot + author: compose-devrel-github-bot + signoff: false + branch: bot-update-deps + delete-branch: true + title: '🤖 Update Dependencies' + body: Updated dependencies + reviewers: ${{ github.actor }} diff --git a/.gitignore b/.gitignore index 30e5f7bdc..7b190dc83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,22 @@ *.iml .gradle /local.properties -/.idea/caches/build_file_checksums.ser -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml +.idea/ .DS_Store -/build +build /captures .externalNativeBuild -.idea/* -/.idea/* +.kotlin + +### Xcode ### +## User settings +xcuserdata/ + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index ae78c113f..000000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
-
-
\ No newline at end of file diff --git a/ASSETS_LICENSE b/ASSETS_LICENSE new file mode 100644 index 000000000..e7fc95866 --- /dev/null +++ b/ASSETS_LICENSE @@ -0,0 +1,88 @@ +All font files are licensed under the SIL OPEN FONT LICENSE license. All other files are licensed under the Apache 2 license. + + +SIL OPEN FONT LICENSE +Version 1.1 - 26 February 2007 + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting — in part or in whole — any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/bluetoothle/build.gradle b/bluetoothle/build.gradle deleted file mode 100644 index aa556fd68..000000000 --- a/bluetoothle/build.gradle +++ /dev/null @@ -1,36 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' - -android { - compileSdkVersion 29 - buildToolsVersion "29.0.3" - - defaultConfig { - minSdkVersion 23 - targetSdkVersion 31 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'consumer-rules.pro' - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' -} diff --git a/bluetoothle/build.gradle.kts b/bluetoothle/build.gradle.kts new file mode 100644 index 000000000..5dc002c96 --- /dev/null +++ b/bluetoothle/build.gradle.kts @@ -0,0 +1,46 @@ + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} +android { + compileSdk = libs.versions.compileSdk.get().toInt() + namespace = "com.sample.android.bluetoothle" + + defaultConfig { + applicationId = "com.sample.android.bluetoothle" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } + buildTypes { + getByName("debug") { + signingConfig = signingConfigs.getByName("debug") + } + + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro") + } + } +} +dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.espresso.core) +} diff --git a/bluetoothle/proguard-rules.pro b/bluetoothle/proguard-rules.pro index f1b424510..2f9dc5a47 100644 --- a/bluetoothle/proguard-rules.pro +++ b/bluetoothle/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/bluetoothle/src/main/AndroidManifest.xml b/bluetoothle/src/main/AndroidManifest.xml index 9830d3a80..b42865576 100644 --- a/bluetoothle/src/main/AndroidManifest.xml +++ b/bluetoothle/src/main/AndroidManifest.xml @@ -1,7 +1,23 @@ - + + + + + @@ -21,6 +37,6 @@ + - - \ No newline at end of file + diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/DeviceScanActivity.java b/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/DeviceScanActivity.java index e453f7032..f9169bf23 100644 --- a/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/DeviceScanActivity.java +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/DeviceScanActivity.java @@ -1,5 +1,6 @@ package com.sample.android.bluetoothle.java; +import android.annotation.SuppressLint; import android.app.ListActivity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.le.BluetoothLeScanner; @@ -35,6 +36,7 @@ public void onScanResult(int callbackType, ScanResult result) { // Stops scanning after 10 seconds. private static final long SCAN_PERIOD = 10000; + @SuppressLint("MissingPermission") private void scanLeDevice() { if (!mScanning) { // Stops scanning after a pre-defined scan period. @@ -54,4 +56,4 @@ public void run() { } } // [END start_and_stop_scan] -} \ No newline at end of file +} diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/MainActivity.java b/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/MainActivity.java index b296ce70b..5e25d58e6 100644 --- a/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/MainActivity.java +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/java/MainActivity.java @@ -1,13 +1,15 @@ package com.sample.android.bluetoothle.java; -import androidx.appcompat.app.AppCompatActivity; - +import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import androidx.annotation.RequiresPermission; +import androidx.appcompat.app.AppCompatActivity; + import com.sample.android.bluetoothle.R; public class MainActivity extends AppCompatActivity { @@ -19,6 +21,7 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private void setUpBLE() { // [START get_bluetooth_adapter] // Initializes Bluetooth adapter. diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceScanActivity.kt b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceScanActivity.kt index fd1662998..585a9e3f5 100644 --- a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceScanActivity.kt +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/DeviceScanActivity.kt @@ -1,10 +1,28 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.sample.android.bluetoothle.kotlin +import android.Manifest import android.app.ListActivity import android.bluetooth.BluetoothAdapter import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult import android.os.Handler +import androidx.annotation.RequiresPermission import com.sample.android.bluetoothle.java.LeDeviceListAdapter /** @@ -31,6 +49,7 @@ class DeviceScanActivity : ListActivity() { // Stops scanning after 10 seconds. private val SCAN_PERIOD: Long = 10000 + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) private fun scanLeDevice() { if (!mScanning) { // Stops scanning after a pre-defined scan period. handler.postDelayed({ @@ -45,4 +64,4 @@ class DeviceScanActivity : ListActivity() { } } // [END start_and_stop_scan] -} \ No newline at end of file +} diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/LeDeviceListAdapter.kt b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/LeDeviceListAdapter.kt index c8670e094..42e37c07e 100644 --- a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/LeDeviceListAdapter.kt +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/LeDeviceListAdapter.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.sample.android.bluetoothle.kotlin import android.bluetooth.BluetoothClass @@ -5,11 +21,11 @@ import android.bluetooth.BluetoothDevice import android.content.Context import android.widget.ArrayAdapter -class LeDeviceListAdapter(context: Context?, layout: Int) - : ArrayAdapter(context!!, layout) { +class LeDeviceListAdapter(context: Context?, layout: Int) : + ArrayAdapter(context!!, layout) { fun addDevice(device: BluetoothDevice?) { // This is where you can add devices to the adapter to // show a list of discovered devices in the UI. } -} \ No newline at end of file +} diff --git a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt index c79aae135..95fe7df91 100644 --- a/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt +++ b/bluetoothle/src/main/java/com/sample/android/bluetoothle/kotlin/MainActivity.kt @@ -1,12 +1,29 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.sample.android.bluetoothle.kotlin +import android.Manifest import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.annotation.RequiresPermission import androidx.appcompat.app.AppCompatActivity -import com.sample.android.bluetoothle.java.MainActivity class MainActivity : AppCompatActivity() { @@ -16,6 +33,7 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun setUpBLE() { // [START get_bluetooth_adapter] // Initializes Bluetooth adapter. diff --git a/bluetoothle/src/main/res/layout/activity_main.xml b/bluetoothle/src/main/res/layout/activity_main.xml index b692bf4a3..b46563beb 100644 --- a/bluetoothle/src/main/res/layout/activity_main.xml +++ b/bluetoothle/src/main/res/layout/activity_main.xml @@ -1,4 +1,19 @@ + - \ No newline at end of file + diff --git a/bluetoothle/src/main/res/values/strings.xml b/bluetoothle/src/main/res/values/strings.xml index 7abc06d3b..fa7796f20 100644 --- a/bluetoothle/src/main/res/values/strings.xml +++ b/bluetoothle/src/main/res/values/strings.xml @@ -1 +1,17 @@ + + diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 77a947201..000000000 --- a/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - ext { - compose_version = '1.2.0-alpha04' - kotlin_version = '1.6.10' - } - repositories { - google() - jcenter() - } - dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..d4ea18313 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,106 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.gradle.versions) + alias(libs.plugins.version.catalog.update) + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.android.kotlin.multiplatform.library) apply false + alias(libs.plugins.android.lint) apply false + alias(libs.plugins.spotless) apply false +} + +allprojects { + apply(plugin = "com.diffplug.spotless") + extensions.configure { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt", "spotless/**/*.kt") + + val disabledRules = arrayOf( + // These rules were introduced in ktlint 0.46.0 and should not be + // enabled without further discussion. They are disabled for now. + // See: https://github.com/pinterest/ktlint/releases/tag/0.46.0 + "filename", + "annotation", + "annotation-spacing", + "argument-list-wrapping", + "double-colon-spacing", + "enum-entry-name-case", + "multiline-if-else", + "no-empty-first-line-in-method-block", + "package-name", + "trailing-comma", + "spacing-around-angle-brackets", + "spacing-between-declarations-with-annotations", + "spacing-between-declarations-with-comments", + "unary-op-spacing", + "no-trailing-spaces", + "max-line-length", + // Disabled rules that were introduced or changed between 0.46.0 ~ 1.50.0 + "class-signature", + "trailing-comma-on-call-site", + "trailing-comma-on-declaration-site", + "comment-wrapping", + "function-literal", + "function-signature", + "function-expression-body", + "function-start-of-body-spacing", + "multiline-expression-wrapping", + ) + + ktlint(libs.versions.ktlint.get()).editorConfigOverride( + mapOf( + "android" to "true", + "ktlint_code_style" to "android_studio", + "ij_kotlin_allow_trailing_comma" to "true", + ) + disabledRules.map { Pair("ktlint_standard_$it", "disabled") } + ) + + // ktlint 7.0.0 introduces lints, which existing snippets do not satisfy + val kotlinSuppressLints = arrayOf( + "standard:function-naming", + "standard:property-naming", + "standard:class-naming", + "standard:max-line-length", + "standard:comment-wrapping", + "standard:import-ordering", + "standard:filename", + "standard:backing-property-naming", + ) + for (lint in kotlinSuppressLints) { + suppressLintsFor { + step = "ktlint" + shortCode = lint + } + } + + licenseHeaderFile(rootProject.file("spotless/copyright.kt")) + } + kotlinGradle { + target("**/*.kts") + targetExclude("**/build/**/*.kts", "spotless/**/*.kts") + // Look for the first line that doesn't have a block comment (assumed to be the license) + licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)") + } + format("xml") { + target("**/*.xml") + targetExclude( + "**/build/**/*.xml", + "spotless/**/*.xml", + ".idea/**", + ) + // Look for the root tag or a tag that is a snippet + licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[a-zA-Z])|( + diff --git a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/MainActivity.kt b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/MainActivity.kt index bdf244ed6..cd3540ab7 100644 --- a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/MainActivity.kt +++ b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/MainActivity.kt @@ -1,20 +1,21 @@ /* - * Copyright 2022 Google Inc. + * Copyright 2022 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.example.android.compose.recomposehighlighter -import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -25,7 +26,7 @@ import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -42,11 +43,12 @@ class MainActivity : ComponentActivity() { @Composable private fun Content() { - var counter by remember { mutableStateOf(0) } + var counter by remember { mutableIntStateOf(0) } Column( Modifier .fillMaxSize() - .padding(16.dp)) { + .padding(16.dp) + ) { Button( onClick = { counter++ }, Modifier.padding(bottom = 12.dp) diff --git a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt index 25dcc4a1e..cc92daed6 100644 --- a/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt +++ b/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt @@ -1,114 +1,159 @@ /* - * Copyright 2022 Google Inc. + * Copyright 2022 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.example.android.compose.recomposehighlighter -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.lerp +import java.util.Objects import kotlin.math.min +import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * A [Modifier] that draws a border around elements that are recomposing. The border increases in * size and interpolates from red to green as more recompositions occur before a timeout. */ @Stable -fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier) - -// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations -// Modifier.composed will still remember unique data per call site. -private val recomposeModifier = - Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) { - // The total number of compositions that have occurred. We're not using a State<> here be - // able to read/write the value without invalidating (which would cause infinite - // recomposition). - val totalCompositions = remember { arrayOf(0L) } - totalCompositions[0]++ - - // The value of totalCompositions at the last timeout. - val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) } - - // Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions - // as the key is really just to cause the timer to restart every composition). - LaunchedEffect(totalCompositions[0]) { +fun Modifier.recomposeHighlighter(): Modifier = this.then(RecomposeHighlighterElement()) + +private class RecomposeHighlighterElement : ModifierNodeElement() { + + override fun InspectorInfo.inspectableProperties() { + debugInspectorInfo { name = "recomposeHighlighter" } + } + + override fun create(): RecomposeHighlighterModifier = RecomposeHighlighterModifier() + + override fun update(node: RecomposeHighlighterModifier) { + node.incrementCompositions() + } + + // It's never equal, so that every recomposition triggers the update function. + override fun equals(other: Any?): Boolean = false + + override fun hashCode(): Int = Objects.hash(this) +} + +private class RecomposeHighlighterModifier : Modifier.Node(), DrawModifierNode { + + private var timerJob: Job? = null + + /** + * The total number of compositions that have occurred. + */ + private var totalCompositions: Long = 0 + set(value) { + if (field == value) return + if (value > 0) restartTimer() + field = value + invalidateDraw() + } + + fun incrementCompositions() { + totalCompositions++ + } + + override fun onAttach() { + super.onAttach() + restartTimer() + } + + override val shouldAutoInvalidate: Boolean = false + + override fun onReset() { + totalCompositions = 0 + timerJob?.cancel() + } + + override fun onDetach() { + timerJob?.cancel() + } + + /** + * Start the timeout, and reset everytime there's a recomposition. + */ + private fun restartTimer() { + if (!isAttached) return + + timerJob?.cancel() + timerJob = coroutineScope.launch { delay(3000) - totalCompositionsAtLastTimeout.value = totalCompositions[0] + totalCompositions = 0 } + } - Modifier.drawWithCache { - onDrawWithContent { - // Draw actual content. - drawContent() + override fun ContentDrawScope.draw() { + // Draw actual content. + drawContent() - // Below is to draw the highlight, if necessary. A lot of the logic is copied from - // Modifier.border - val numCompositionsSinceTimeout = - totalCompositions[0] - totalCompositionsAtLastTimeout.value + // Below is to draw the highlight, if necessary. A lot of the logic is copied from Modifier.border - val hasValidBorderParams = size.minDimension > 0f - if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) { - return@onDrawWithContent - } + val hasValidBorderParams = size.minDimension > 0f + if (!hasValidBorderParams || totalCompositions <= 0) { + return + } - val (color, strokeWidthPx) = - when (numCompositionsSinceTimeout) { - // We need at least one composition to draw, so draw the smallest border - // color in blue. - 1L -> Color.Blue to 1f - // 2 compositions is _probably_ okay. - 2L -> Color.Green to 2.dp.toPx() - // 3 or more compositions before timeout may indicate an issue. lerp the - // color from yellow to red, and continually increase the border size. - else -> { - lerp( - Color.Yellow.copy(alpha = 0.8f), - Color.Red.copy(alpha = 0.5f), - min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f) - ) to numCompositionsSinceTimeout.toInt().dp.toPx() - } - } - - val halfStroke = strokeWidthPx / 2 - val topLeft = Offset(halfStroke, halfStroke) - val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx) - - val fillArea = (strokeWidthPx * 2) > size.minDimension - val rectTopLeft = if (fillArea) Offset.Zero else topLeft - val size = if (fillArea) size else borderSize - val style = if (fillArea) Fill else Stroke(strokeWidthPx) - - drawRect( - brush = SolidColor(color), - topLeft = rectTopLeft, - size = size, - style = style - ) + val (color, strokeWidthPx) = + when (totalCompositions) { + // We need at least one composition to draw, so draw the smallest border + // color in blue. + 1L -> Color.Blue to 1f + // 2 compositions is _probably_ okay. + 2L -> Color.Green to 2.dp.toPx() + // 3 or more compositions before timeout may indicate an issue. lerp the + // color from yellow to red, and continually increase the border size. + else -> { + lerp( + Color.Yellow.copy(alpha = 0.8f), + Color.Red.copy(alpha = 0.5f), + min(1f, (totalCompositions - 1).toFloat() / 100f), + ) to totalCompositions.toInt().dp.toPx() + } } - } + + val halfStroke = strokeWidthPx / 2 + val topLeft = Offset(halfStroke, halfStroke) + val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx) + + val fillArea = (strokeWidthPx * 2) > size.minDimension + val rectTopLeft = if (fillArea) Offset.Zero else topLeft + val size = if (fillArea) size else borderSize + val style = if (fillArea) Fill else Stroke(strokeWidthPx) + + drawRect( + brush = SolidColor(color), + topLeft = rectTopLeft, + size = size, + style = style, + ) } +} diff --git a/compose/recomposehighlighter/src/main/res/values/strings.xml b/compose/recomposehighlighter/src/main/res/values/strings.xml index 3c88fdee7..c124e7ac2 100644 --- a/compose/recomposehighlighter/src/main/res/values/strings.xml +++ b/compose/recomposehighlighter/src/main/res/values/strings.xml @@ -1,15 +1,19 @@ + + Copyright 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> RecomposeHighlighter diff --git a/compose/recomposehighlighter/src/main/res/values/themes.xml b/compose/recomposehighlighter/src/main/res/values/themes.xml index e14b39c1e..f94279433 100644 --- a/compose/recomposehighlighter/src/main/res/values/themes.xml +++ b/compose/recomposehighlighter/src/main/res/values/themes.xml @@ -1,6 +1,21 @@ + + - - \ No newline at end of file + diff --git a/compose/snippets/.gitignore b/compose/snippets/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/compose/snippets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/compose/snippets/build.gradle.kts b/compose/snippets/build.gradle.kts new file mode 100644 index 000000000..7a5062f65 --- /dev/null +++ b/compose/snippets/build.gradle.kts @@ -0,0 +1,170 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) +} + +android { + compileSdk { + version = release(libs.versions.compileSdk.get().toInt()) + {minorApiLevel = 1}} // Android 16 QPR 2 + namespace = "com.example.compose.snippets" + + defaultConfig { + applicationId = "com.example.compose.snippets" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + getByName("debug") { + signingConfig = signingConfigs.getByName("debug") + } + + getByName("release") { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + jvmToolchain(17) + } + + buildFeatures { + compose = true + // Disable unused AGP features + viewBinding = true + } + + packaging.resources { + // Multiple dependency bring these files in. Exclude them to enable + // our test APK to build (has no effect on our AARs) + excludes += "/META-INF/AL2.0" + excludes += "/META-INF/LGPL2.1" + } + lint { + lintConfig = file("lint.xml") + } +} + +dependencies { + implementation(libs.androidx.work.runtime.ktx) + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + androidTestImplementation(composeBom) + + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.util) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.graphics.shapes) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.ui.viewbinding) + implementation(libs.androidx.paging.compose) + implementation(libs.androidx.compose.animation.graphics) + + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material3.adaptive.layout) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation.suite) + implementation(libs.androidx.compose.material) + + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.runtime.livedata) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.material.ripple) + implementation(libs.androidx.constraintlayout.compose) + implementation(libs.androidx.compose.ui.googlefonts) + + implementation(libs.androidx.emoji2.views) + implementation(libs.androidx.lifecycle.runtime.compose) + + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.coordinator.layout) + implementation(libs.google.android.material) + + implementation(libs.androidx.glance.appwidget) + implementation(libs.androidx.glance.material3) + + implementation(libs.androidx.window.core) + + implementation(libs.accompanist.theme.adapter.appcompat) + implementation(libs.accompanist.theme.adapter.material3) + implementation(libs.accompanist.theme.adapter.material) + + implementation(libs.accompanist.permissions) + + implementation(libs.coil.kt.compose) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.core.ktx) + + implementation(libs.androidx.lifecycle.runtime) + implementation(libs.androidx.lifecycle.viewModelCompose) + + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.exoplayer) + + implementation(libs.androidx.navigation.compose) + implementation(libs.hilt.android) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.kotlinx.serialization.json) + + implementation(libs.androidx.recyclerview) + + implementation(libs.googlemaps.compose) + implementation(libs.googlemaps.maps) + + implementation(libs.hilt.android) + implementation(libs.glide.compose) + + ksp(libs.hilt.compiler) + + testImplementation(libs.junit) + + androidTestImplementation(composeBom) + + androidTestImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.compose.ui.test) + debugImplementation(libs.androidx.compose.ui.tooling) + + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.compose.ui.test.junit4.accessibility) + debugImplementation(libs.androidx.compose.ui.test.manifest) + + androidTestImplementation(libs.androidx.glance.testing) + androidTestImplementation(libs.androidx.glance.appwidget.testing) +} diff --git a/compose/snippets/lint.xml b/compose/snippets/lint.xml new file mode 100644 index 000000000..9ec5c3429 --- /dev/null +++ b/compose/snippets/lint.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/compose/snippets/proguard-rules.pro b/compose/snippets/proguard-rules.pro new file mode 100644 index 000000000..ff59496d8 --- /dev/null +++ b/compose/snippets/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt new file mode 100644 index 000000000..ee6b51325 --- /dev/null +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.accessibility + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.junit4.accessibility.enableAccessibilityChecks +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.tryPerformAccessibilityChecks +import androidx.compose.ui.unit.dp +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult +import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +class AccessibilityTest { + +// [START android_compose_accessibility_testing_label] + @Rule + @JvmField + val composeTestRule = createAndroidComposeRule() + + @Test + fun noAccessibilityLabel() { + composeTestRule.setContent { + Box( + modifier = Modifier + .size(50.dp, 50.dp) + .background(color = Color.Gray) + .clickable { } + .semantics { + contentDescription = "" + } + ) + } + + composeTestRule.enableAccessibilityChecks() + + // Any action (such as performClick) will perform accessibility checks too: + composeTestRule.onRoot().tryPerformAccessibilityChecks() + } +// [END android_compose_accessibility_testing_label] + +// [START android_compose_accessibility_testing_click] + @Test + fun smallClickTarget() { + composeTestRule.setContent { + Box( + modifier = Modifier + .size(20.dp, 20.dp) + .background(color = Color(0xFFFAFBFC)) + .clickable { } + ) + } + + composeTestRule.enableAccessibilityChecks() + + // Any action (such as performClick) will perform accessibility checks too: + composeTestRule.onRoot().tryPerformAccessibilityChecks() + } +// [END android_compose_accessibility_testing_click] + +// [START android_compose_accessibility_testing_validator] + @Test + fun lowContrastScreen() { + composeTestRule.setContent { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = Color(0xFFFAFBFC)), + contentAlignment = Alignment.Center + ) { + Text(text = "Hello", color = Color(0xFFB0B1B2)) + } + } + + // Optionally, set AccessibilityValidator manually + val accessibilityValidator = AccessibilityValidator() + .setThrowExceptionFor( + AccessibilityCheckResult.AccessibilityCheckResultType.WARNING + ) + + composeTestRule.enableAccessibilityChecks(accessibilityValidator) + + composeTestRule.onRoot().tryPerformAccessibilityChecks() + } +// [END android_compose_accessibility_testing_validator] + + private val nodeMatcher = SemanticsMatcher(description = "DUMMY") { it.isRoot } + + @Ignore("Dummy test") +// [START android_compose_accessibility_testing] + @Test + fun test() { + composeTestRule + .onNode(nodeMatcher) + .assert( + SemanticsMatcher("onClickLabel is set correctly") { + it.config.getOrNull(SemanticsActions.OnClick)?.label == "My Click Label" + } + ) + } +// [END android_compose_accessibility_testing] +} diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/animation/AnimationTestingSnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/animation/AnimationTestingSnippets.kt new file mode 100644 index 000000000..2241a2e8d --- /dev/null +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/animation/AnimationTestingSnippets.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("UnusedReceiverParameter") + +package com.example.compose.snippets.animation + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.test.captureToImage +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.unit.dp +import org.junit.Rule +import org.junit.Test + +/* +* Copyright 2023 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +class AnimationTestingSnippets { + // [START android_compose_animations_testing_example] + @get:Rule + val rule = createComposeRule() + + @Test + fun testAnimationWithClock() { + // Pause animations + rule.mainClock.autoAdvance = false + var enabled by mutableStateOf(false) + rule.setContent { + val color by animateColorAsState( + targetValue = if (enabled) Color.Red else Color.Green, + animationSpec = tween(durationMillis = 250) + ) + Box(Modifier.size(64.dp).background(color)) + } + + // Initiate the animation. + enabled = true + + // Let the animation proceed. + rule.mainClock.advanceTimeBy(50L) + + // Compare the result with the image showing the expected result. + // `assertAgainGolden` needs to be implemented in your code. + rule.onRoot().captureToImage().assertAgainstGolden() + } + // [END android_compose_animations_testing_example] +} + +/* +Fakes needed for snippets to build: + */ + +private fun ImageBitmap.assertAgainstGolden() { +} diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/deviceconfigurationoverride/DeviceConfigurationOverrideSnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/deviceconfigurationoverride/DeviceConfigurationOverrideSnippets.kt new file mode 100644 index 000000000..fca68ad4f --- /dev/null +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/deviceconfigurationoverride/DeviceConfigurationOverrideSnippets.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.deviceconfigurationoverride + +import androidx.compose.material3.Text +import androidx.compose.ui.test.DeviceConfigurationOverride +import androidx.compose.ui.test.FontScale +import androidx.compose.ui.test.FontWeightAdjustment +import androidx.compose.ui.test.ForcedSize +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.then +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.interop.MyScreen +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +class DeviceConfigurationOverrideSnippetsTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Ignore("Snippet test") + @Test + fun forcedSize() { + // [START android_compose_deviceconfigurationoverride_forcedsize] + composeTestRule.setContent { + DeviceConfigurationOverride( + DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp)) + ) { + MyScreen() // Will be rendered in the space for 1280dp by 800dp without clipping. + } + } + // [END android_compose_deviceconfigurationoverride_forcedsize] + } + + @Ignore("Snippet test") + @Test + fun then() { + // [START android_compose_deviceconfigurationoverride_then] + composeTestRule.setContent { + DeviceConfigurationOverride( + DeviceConfigurationOverride.FontScale(1.5f) then + DeviceConfigurationOverride.FontWeightAdjustment(200) + ) { + Text(text = "text with increased scale and weight") + } + } + // [END android_compose_deviceconfigurationoverride_then] + } +} diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/glance/GlanceUnitTest.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/glance/GlanceUnitTest.kt new file mode 100644 index 000000000..05698d1a3 --- /dev/null +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/glance/GlanceUnitTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.glance + +import androidx.compose.runtime.Composable +import androidx.glance.GlanceModifier +import androidx.glance.appwidget.testing.unit.runGlanceAppWidgetUnitTest +import androidx.glance.layout.Row +import androidx.glance.semantics.semantics +import androidx.glance.semantics.testTag +import androidx.glance.testing.unit.assertHasText +import androidx.glance.testing.unit.hasTestTag +import androidx.glance.text.Text +import org.junit.Test + +// [START android_compose_glance_unit_test] +private const val FAKE_HEADLINE = "EXTRA! EXTRA! READ ALL ABOUT IT!" + +class MyGlanceComposableTest { + @Test + fun myNewsItemComposable_largeSize_hasHeadline() = runGlanceAppWidgetUnitTest { + // Set the composable to test + provideComposable { + MyNewsItemComposable(FAKE_HEADLINE) + } + + // Perform assertions + onNode(hasTestTag("headline")) + .assertHasText(FAKE_HEADLINE) + } + + + @Composable + fun MyNewsItemComposable(headline: String) { + Row { + Text( + text = headline, + modifier = GlanceModifier.semantics { testTag = "headline" }, + ) + } + } +} +// [END android_compose_glance_unit_test] \ No newline at end of file diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/interop/AddingComposeToYourAppSnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/interop/AddingComposeToYourAppSnippets.kt new file mode 100644 index 000000000..726b7794d --- /dev/null +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/interop/AddingComposeToYourAppSnippets.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.interop + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.platform.app.InstrumentationRegistry +import com.example.compose.snippets.MyActivity +import com.example.compose.snippets.R +import org.junit.Rule +import org.junit.Test + +// [START android_compose_interop_add_compose_test_mixed] +class MyActivityTest { + @Rule + @JvmField + val composeTestRule = createAndroidComposeRule() + + @Test + fun testGreeting() { + val greeting = InstrumentationRegistry.getInstrumentation() + .targetContext.resources.getString(R.string.greeting) + + composeTestRule.onNodeWithText(greeting).assertIsDisplayed() + } +} +// [END android_compose_interop_add_compose_test_mixed] diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt new file mode 100644 index 000000000..89b577ea7 --- /dev/null +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.semantics + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.printToLog +import org.junit.Rule +import org.junit.Test + +@Suppress("TestFunctionName") +// [START android_compose_semantics_logging] +class MyComposeTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun MyTest() { + // Start the app + composeTestRule.setContent { + MyTheme { + Text("Hello world!") + } + } + // Log the full semantics tree + composeTestRule.onRoot().printToLog("MY TAG") + } +} +// [END android_compose_semantics_logging] + +class Test2 { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testSwitch() { + // [START android_compose_semantics_test_switch] + val mySwitch = SemanticsMatcher.expectValue( + SemanticsProperties.Role, Role.Switch + ) + composeTestRule.onNode(mySwitch) + .performClick() + .assertIsOff() + // [END android_compose_semantics_test_switch] + } +} + +class Test3 { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testLog() { + // [START android_compose_semantics_print_log] + composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG") + // [END android_compose_semantics_print_log] + // [START android_compose_semantics_match_text] + composeTestRule.onNodeWithText("Like").performClick() + // [END android_compose_semantics_match_text] + } +} + +@Suppress("TestFunctionName") +@Composable +private fun MyTheme( + content: @Composable () -> Unit +) { + content() +} diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/touchinput/pointerinput/UnderstandGesturesSnippets.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/touchinput/pointerinput/UnderstandGesturesSnippets.kt new file mode 100644 index 000000000..4b204b232 --- /dev/null +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/touchinput/pointerinput/UnderstandGesturesSnippets.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.touchinput.pointerinput + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.test.click +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import androidx.compose.ui.test.swipeUp +import com.example.compose.snippets.designsystems.MyTheme +import org.junit.Rule +import org.junit.Test + +@Suppress("TestFunctionName") +class GesturesTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun MyTest() { + // Start the app + composeTestRule.setContent { + MyTheme { + Text("Hello world!") + } + } + + // [START android_compose_touchinput_pointerinput_test] + composeTestRule.onNodeWithTag("MyList").performTouchInput { + swipeUp() + swipeDown() + click() + } + // [END android_compose_touchinput_pointerinput_test] + } +} + +@Suppress("TestFunctionName") +@Composable +private fun MyTheme( + content: @Composable () -> Unit +) { + content() +} diff --git a/compose/snippets/src/main/AndroidManifest.xml b/compose/snippets/src/main/AndroidManifest.xml new file mode 100644 index 000000000..5c0f57b2e --- /dev/null +++ b/compose/snippets/src/main/AndroidManifest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/MyActivity.kt b/compose/snippets/src/main/java/com/example/compose/snippets/MyActivity.kt new file mode 100644 index 000000000..ff2c26bf9 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/MyActivity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.Text +import androidx.compose.ui.res.stringResource + +class MyActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + Text(text = stringResource(id = R.string.greeting)) + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt new file mode 100644 index 000000000..48f2a1109 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets + +import NavigationDrawerExamples +import android.os.Bundle +import android.os.StrictMode +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.example.compose.snippets.animations.AnimationExamplesScreen +import com.example.compose.snippets.animations.sharedelement.PlaceholderSizeAnimated_Demo +import com.example.compose.snippets.components.AppBarExamples +import com.example.compose.snippets.components.BadgeExamples +import com.example.compose.snippets.components.ButtonExamples +import com.example.compose.snippets.components.CardExamples +import com.example.compose.snippets.components.CarouselExamples +import com.example.compose.snippets.components.CheckboxExamples +import com.example.compose.snippets.components.ChipExamples +import com.example.compose.snippets.components.ComponentsScreen +import com.example.compose.snippets.components.DatePickerExamples +import com.example.compose.snippets.components.DialogExamples +import com.example.compose.snippets.components.DividerExamples +import com.example.compose.snippets.components.FloatingActionButtonExamples +import com.example.compose.snippets.components.MenusExamples +import com.example.compose.snippets.components.PartialBottomSheet +import com.example.compose.snippets.components.ProgressIndicatorExamples +import com.example.compose.snippets.components.ScaffoldExample +import com.example.compose.snippets.components.SearchBarExamples +import com.example.compose.snippets.components.SegmentedButtonExamples +import com.example.compose.snippets.components.SliderExamples +import com.example.compose.snippets.components.SwipeToDismissBoxExamples +import com.example.compose.snippets.components.SwitchExamples +import com.example.compose.snippets.components.TimePickerExamples +import com.example.compose.snippets.components.TooltipExamples +import com.example.compose.snippets.graphics.ApplyPolygonAsClipImage +import com.example.compose.snippets.graphics.BitmapFromComposableFullSnippet +import com.example.compose.snippets.graphics.BrushExamplesScreen +import com.example.compose.snippets.images.ImageExamplesScreen +import com.example.compose.snippets.landing.LandingScreen +import com.example.compose.snippets.layouts.PagerExamples +import com.example.compose.snippets.navigation.Destination +import com.example.compose.snippets.navigation.TopComponentsDestination +import com.example.compose.snippets.ui.theme.SnippetsTheme + +class SnippetsActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + StrictMode.enableDefaults() + setContent { + SnippetsTheme { + val navController = rememberNavController() + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + NavHost(navController, startDestination = "LandingScreen") { + composable("LandingScreen") { + LandingScreen { navController.navigate(it.route) } + } + Destination.entries.forEach { destination -> + composable(destination.route) { + when (destination) { + Destination.BrushExamples -> BrushExamplesScreen() + Destination.ImageExamples -> ImageExamplesScreen() + Destination.AnimationQuickGuideExamples -> AnimationExamplesScreen() + Destination.ScreenshotExample -> BitmapFromComposableFullSnippet() + Destination.ComponentsExamples -> ComponentsScreen { + navController.navigate( + it.route + ) + } + Destination.ShapesExamples -> ApplyPolygonAsClipImage() + Destination.SharedElementExamples -> PlaceholderSizeAnimated_Demo() + Destination.PagerExamples -> PagerExamples() + } + } + } + TopComponentsDestination.entries.forEach { destination -> + composable(destination.route) { + when (destination) { + TopComponentsDestination.CardExamples -> CardExamples() + TopComponentsDestination.SwitchExamples -> SwitchExamples() + TopComponentsDestination.SliderExamples -> SliderExamples() + TopComponentsDestination.DialogExamples -> DialogExamples() + TopComponentsDestination.ChipExamples -> ChipExamples() + TopComponentsDestination.FloatingActionButtonExamples -> FloatingActionButtonExamples() + TopComponentsDestination.ButtonExamples -> ButtonExamples() + TopComponentsDestination.ProgressIndicatorExamples -> ProgressIndicatorExamples() + TopComponentsDestination.ScaffoldExample -> ScaffoldExample() + TopComponentsDestination.AppBarExamples -> AppBarExamples { + navController.popBackStack() + } + TopComponentsDestination.CheckboxExamples -> CheckboxExamples() + TopComponentsDestination.DividerExamples -> DividerExamples() + TopComponentsDestination.BadgeExamples -> BadgeExamples() + TopComponentsDestination.PartialBottomSheet -> PartialBottomSheet() + TopComponentsDestination.TimePickerExamples -> TimePickerExamples() + TopComponentsDestination.DatePickerExamples -> DatePickerExamples() + TopComponentsDestination.CarouselExamples -> CarouselExamples() + TopComponentsDestination.MenusExample -> MenusExamples() + TopComponentsDestination.TooltipExamples -> TooltipExamples() + TopComponentsDestination.NavigationDrawerExamples -> NavigationDrawerExamples() + TopComponentsDestination.SegmentedButtonExamples -> SegmentedButtonExamples() + TopComponentsDestination.SwipeToDismissBoxExamples -> SwipeToDismissBoxExamples() + TopComponentsDestination.SearchBarExamples -> SearchBarExamples() + } + } + } + } + } + } + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt new file mode 100644 index 000000000..a83111f71 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/accessibility/AccessibilitySnippets.kt @@ -0,0 +1,838 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "UNUSED_PARAMETER", "ClassName") + +package com.example.compose.snippets.accessibility + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.progressBarRangeInfo +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +@Preview +// [START android_compose_accessibility_checkbox_expanded_touch_target] +@Composable +private fun CheckableCheckbox() { + Checkbox(checked = true, onCheckedChange = {}) +} +// [END android_compose_accessibility_checkbox_expanded_touch_target] + +@Preview +// [START android_compose_accessibility_checkbox_no_touch_target] +@Composable +private fun NonClickableCheckbox() { + Checkbox(checked = true, onCheckedChange = null) +} +// [END android_compose_accessibility_checkbox_no_touch_target] + +@Preview +// [START android_compose_accessibility_checkable_row] +@Composable +private fun CheckableRow() { + MaterialTheme { + var checked by remember { mutableStateOf(false) } + Row( + Modifier + .toggleable( + value = checked, + role = Role.Checkbox, + onValueChange = { checked = !checked } + ) + .padding(16.dp) + .fillMaxWidth() + ) { + Text("Option", Modifier.weight(1f)) + Checkbox(checked = checked, onCheckedChange = null) + } + } +} +// [END android_compose_accessibility_checkable_row] + +@Preview +// [START android_compose_accessibility_box_touch_target] +@Composable +private fun SmallBox() { + var clicked by remember { mutableStateOf(false) } + Box( + Modifier + .size(100.dp) + .background(if (clicked) Color.DarkGray else Color.LightGray) + ) { + Box( + Modifier + .align(Alignment.Center) + .clickable { clicked = !clicked } + .background(Color.Black) + .size(1.dp) + ) + } +} +// [END android_compose_accessibility_box_touch_target] + +@Preview +// [START android_compose_accessibility_box_min_size] +@Composable +private fun LargeBox() { + var clicked by remember { mutableStateOf(false) } + Box( + Modifier + .size(100.dp) + .background(if (clicked) Color.DarkGray else Color.LightGray) + ) { + Box( + Modifier + .align(Alignment.Center) + .clickable { clicked = !clicked } + .background(Color.Black) + .sizeIn(minWidth = 48.dp, minHeight = 48.dp) + ) + } +} +// [END android_compose_accessibility_box_min_size] + +// [START android_compose_accessibility_click_label] +@Composable +private fun ArticleListItem(openArticle: () -> Unit = {}) { + Row( + Modifier.clickable( + // R.string.action_read_article = "read article" + onClickLabel = stringResource(R.string.action_read_article), + onClick = openArticle + ) + ) { + // .. + } +} +// [END android_compose_accessibility_click_label] + +// [START android_compose_accessibility_low_level_click] +@Composable +private fun LowLevelClickLabel(openArticle: () -> Boolean) { + // R.string.action_read_article = "read article" + val readArticleLabel = stringResource(R.string.action_read_article) + Canvas( + Modifier.semantics { + onClick(label = readArticleLabel, action = openArticle) + } + ) { + // .. + } +} +// [END android_compose_accessibility_low_level_click] + +// [START android_compose_accessibility_content_descr] +@Composable +private fun ShareButton(onClick: () -> Unit) { + IconButton(onClick = onClick) { + Icon( + imageVector = Icons.Filled.Share, + contentDescription = stringResource(R.string.label_share) + ) + } +} +// [END android_compose_accessibility_content_descr] + +private object ContentDescrNull { + // hard-code drawable id + private object R { + object drawable { + const val placeholder_1_1 = 1 + } + } + + // [START android_compose_accessibility_content_descr_null] + @Composable + private fun PostImage(post: Post, modifier: Modifier = Modifier) { + val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1) + + Image( + painter = image, + // Specify that this image has no semantic meaning + contentDescription = null, + modifier = modifier + .size(40.dp, 40.dp) + .clip(MaterialTheme.shapes.small) + ) + } +// [END android_compose_accessibility_content_descr_null] +} + +// [START android_compose_accessibility_merge] +@Composable +private fun PostMetadata(metadata: Metadata) { + // Merge elements below for accessibility purposes + Row(modifier = Modifier.semantics(mergeDescendants = true) {}) { + Image( + imageVector = Icons.Filled.AccountCircle, + contentDescription = null // decorative + ) + Column { + Text(metadata.author.name) + Text("${metadata.date} • ${metadata.readTimeMinutes} min read") + } + } +} +// [END android_compose_accessibility_merge] + +// [START android_compose_accessibility_custom_action] +@Composable +private fun PostCardSimple( + /* ... */ + isFavorite: Boolean, + onToggleFavorite: () -> Boolean +) { + val actionLabel = stringResource( + if (isFavorite) R.string.unfavorite else R.string.favorite + ) + Row( + modifier = Modifier + .clickable(onClick = { /* ... */ }) + .semantics { + // Set any explicit semantic properties + customActions = listOf( + CustomAccessibilityAction(actionLabel, onToggleFavorite) + ) + } + ) { + /* ... */ + BookmarkButton( + isBookmarked = isFavorite, + onClick = onToggleFavorite, + // Clear any semantics properties set on this node + modifier = Modifier.clearAndSetSemantics { } + ) + } +} +// [END android_compose_accessibility_custom_action] + +// [START android_compose_accessibility_state_descr] +@Composable +private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) { + val stateSubscribed = stringResource(R.string.subscribed) + val stateNotSubscribed = stringResource(R.string.not_subscribed) + Row( + modifier = Modifier + .semantics { + // Set any explicit semantic properties + stateDescription = if (selected) stateSubscribed else stateNotSubscribed + } + .toggleable( + value = selected, + onValueChange = { onToggle() } + ) + ) { + /* ... */ + } +} +// [END android_compose_accessibility_state_descr] + +// [START android_compose_accessibility_headings] +@Composable +private fun Subsection(text: String) { + Text( + text = text, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.semantics { heading() } + ) +} +// [END android_compose_accessibility_headings] + +// [START android_compose_accessibility_traversal_isTraversalGroup_initial] +// CardBox() function takes in top and bottom sample text. +@Composable +fun CardBox( + topSampleText: String, + bottomSampleText: String, + modifier: Modifier = Modifier +) { + Box(modifier) { + Column { + Text(topSampleText) + Text(bottomSampleText) + } + } +} + +@Composable +fun TraversalGroupDemo() { + val topSampleText1 = "This sentence is in " + val bottomSampleText1 = "the left column." + val topSampleText2 = "This sentence is " + val bottomSampleText2 = "on the right." + Row { + CardBox( + topSampleText1, + bottomSampleText1 + ) + CardBox( + topSampleText2, + bottomSampleText2 + ) + } +} +// [END android_compose_accessibility_traversal_isTraversalGroup_initial] + +// [START android_compose_accessibility_traversal_isTraversalGroup_modified] +@Composable +fun TraversalGroupDemo2() { + val topSampleText1 = "This sentence is in " + val bottomSampleText1 = "the left column." + val topSampleText2 = "This sentence is" + val bottomSampleText2 = "on the right." + Row { + CardBox( +// 1, + topSampleText1, + bottomSampleText1, + Modifier.semantics { isTraversalGroup = true } + ) + CardBox( +// 2, + topSampleText2, + bottomSampleText2, + Modifier.semantics { isTraversalGroup = true } + ) + } +} +// [END android_compose_accessibility_traversal_isTraversalGroup_modified] + +private object ClockFaceBefore { + // [START android_compose_accessibility_traversal_clock_face_initial] + @Composable + fun ClockFaceDemo() { + CircularLayout { + repeat(12) { hour -> + ClockText(hour) + } + } + } + + @Composable + private fun ClockText(value: Int) { + Box(modifier = Modifier) { + Text((if (value == 0) 12 else value).toString()) + } + } + // [END android_compose_accessibility_traversal_clock_face_initial] + + @Composable + private fun CircularLayout(content: @Composable () -> Unit) { + } +} + +private object ClockFaceAfter { + // [START android_compose_accessibility_traversal_clock_face_modified] + @Composable + fun ClockFaceDemo() { + CircularLayout(Modifier.semantics { isTraversalGroup = true }) { + repeat(12) { hour -> + ClockText(hour) + } + } + } + + @Composable + private fun ClockText(value: Int) { + Box(modifier = Modifier.semantics { this.traversalIndex = value.toFloat() }) { + Text((if (value == 0) 12 else value).toString()) + } + } + // [END android_compose_accessibility_traversal_clock_face_modified] + + @Composable + private fun CircularLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + } +} + +// [START android_compose_accessibility_traversal_fab] +@Composable +fun FloatingBox() { + Box(modifier = Modifier.semantics { isTraversalGroup = true; traversalIndex = -1f }) { + FloatingActionButton(onClick = {}) { + Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon") + } + } +} +// [END android_compose_accessibility_traversal_fab] + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun InteractiveElements( + openArticle: () -> Unit = {}, + addToBookmarks: () -> Unit = {}, +) { +// [START android_compose_accessibility_interactive_clickable] + Row( + // Uses `mergeDescendants = true` under the hood + modifier = Modifier.clickable { openArticle() } + ) { + Icon( + painter = painterResource(R.drawable.ic_logo), + contentDescription = "Open", + ) + Text("Accessibility in Compose") + } +// [END android_compose_accessibility_interactive_clickable] + +// [START android_compose_accessibility_interactive_click_label] + Row( + modifier = Modifier + .clickable(onClickLabel = "Open this article") { + openArticle() + } + ) { + Icon( + painter = painterResource(R.drawable.ic_logo), + contentDescription = "Open" + ) + Text("Accessibility in Compose") + } +// [END android_compose_accessibility_interactive_click_label] + +// [START android_compose_accessibility_interactive_long_click] + Row( + modifier = Modifier + .combinedClickable( + onLongClickLabel = "Bookmark this article", + onLongClick = { addToBookmarks() }, + onClickLabel = "Open this article", + onClick = { openArticle() }, + ) + ) {} +// [END android_compose_accessibility_interactive_long_click] +} + +// [START android_compose_accessibility_interactive_nested_click] +@Composable +private fun ArticleList(openArticle: () -> Unit) { + NestedArticleListItem( + // Clickable is set separately, in a nested layer: + onClickAction = openArticle, + // Semantics are set here: + modifier = Modifier.semantics { + onClick( + label = "Open this article", + action = { + // Not needed here: openArticle() + true + } + ) + } + ) +} +// [END android_compose_accessibility_interactive_nested_click] + +@Composable +private fun NestedArticleListItem( + onClickAction: () -> Unit, + modifier: Modifier = Modifier, +) { +} + +@Composable +private fun Semantics( + removeArticle: () -> Unit, + openArticle: () -> Unit, + addToBookmarks: () -> Unit, +) { + +// [START android_compose_accessibility_semantics_alert_polite] + PopupAlert( + message = "You have a new message", + modifier = Modifier.semantics { + liveRegion = LiveRegionMode.Polite + } + ) +// [END android_compose_accessibility_semantics_alert_polite] + +// [START android_compose_accessibility_semantics_alert_assertive] + PopupAlert( + message = "Emergency alert incoming", + modifier = Modifier.semantics { + liveRegion = LiveRegionMode.Assertive + } + ) +// [END android_compose_accessibility_semantics_alert_assertive] + + Box() { +// [START android_compose_accessibility_semantics_window] + ShareSheet( + message = "Choose how to share this photo", + modifier = Modifier + .fillMaxWidth() + .align(Alignment.TopCenter) + .semantics { paneTitle = "New bottom sheet" } + ) +// [END android_compose_accessibility_semantics_window] + } + +// [START android_compose_accessibility_semantics_error] + Error( + errorText = "Fields cannot be empty", + modifier = Modifier + .semantics { + error("Please add both email and password") + } + ) +// [END android_compose_accessibility_semantics_error] + + val progress by remember { mutableFloatStateOf(0F) } +// [START android_compose_accessibility_semantics_progress] + ProgressInfoBar( + modifier = Modifier + .semantics { + progressBarRangeInfo = + ProgressBarRangeInfo( + current = progress, + range = 0F..1F + ) + } + ) +// [END android_compose_accessibility_semantics_progress] + + val milkyWay = List(10) { it.toString() } +// [START android_compose_accessibility_semantics_long_list] + MilkyWayList( + modifier = Modifier + .semantics { + collectionInfo = CollectionInfo( + rowCount = milkyWay.count(), + columnCount = 1 + ) + } + ) { + milkyWay.forEachIndexed { index, text -> + Text( + text = text, + modifier = Modifier.semantics { + collectionItemInfo = + CollectionItemInfo(index, 0, 0, 0) + } + ) + } + } +// [END android_compose_accessibility_semantics_long_list] + +// [START android_compose_accessibility_semantics_custom_action_swipe] + SwipeToDismissBox( + modifier = Modifier.semantics { + // Represents the swipe to dismiss for accessibility + customActions = listOf( + CustomAccessibilityAction( + label = "Remove article from list", + action = { + removeArticle() + true + } + ) + ) + }, + state = rememberSwipeToDismissBoxState(), + backgroundContent = {} + ) { + ArticleListItem() + } +// [END android_compose_accessibility_semantics_custom_action_swipe] + +// [START android_compose_accessibility_semantics_custom_action_long_list] + ArticleListItemRow( + modifier = Modifier + .semantics { + customActions = listOf( + CustomAccessibilityAction( + label = "Open article", + action = { + openArticle() + true + } + ), + CustomAccessibilityAction( + label = "Add to bookmarks", + action = { + addToBookmarks() + true + } + ), + ) + } + ) { + Article( + modifier = Modifier.clearAndSetSemantics { }, + onClick = openArticle, + ) + BookmarkButton( + modifier = Modifier.clearAndSetSemantics { }, + onClick = addToBookmarks, + ) + } +// [END android_compose_accessibility_semantics_custom_action_long_list] +} + +@Composable +private fun PopupAlert( + message: String, + modifier: Modifier = Modifier, +) { +} + +@Composable +fun ShareSheet( + message: String, + modifier: Modifier = Modifier, +) { +} + +@Composable +private fun Error( + errorText: String, + modifier: Modifier = Modifier, +) { +} + +@Composable +private fun ProgressInfoBar( + modifier: Modifier = Modifier, +) { +} + +@Composable +private fun MilkyWayList( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + content() +} + +@Composable +private fun ArticleListItemRow( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + content() +} + +@Composable +fun Article( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { +} + +@Composable +fun BookmarkButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { +} + +// [START android_compose_accessibility_merging] +@Composable +private fun ArticleListItem( + openArticle: () -> Unit, + addToBookmarks: () -> Unit, +) { + + Row(modifier = Modifier.clickable { openArticle() }) { + // Merges with parent clickable: + Icon( + painter = painterResource(R.drawable.ic_logo), + contentDescription = "Article thumbnail" + ) + ArticleDetails() + + // Defies the merge due to its own clickable: + BookmarkButton(onClick = addToBookmarks) + } +} +// [END android_compose_accessibility_merging] + +@Composable +fun ArticleDetails( + modifier: Modifier = Modifier, +) { +} + +// [START android_compose_accessibility_clearing] +// Developer might intend this to be a toggleable. +// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied, +// a custom description is set, and a Role is applied. + +@Composable +fun FavoriteToggle() { + val checked = remember { mutableStateOf(true) } + Row( + modifier = Modifier + .toggleable( + value = checked.value, + onValueChange = { checked.value = it } + ) + .clearAndSetSemantics { + stateDescription = if (checked.value) "Favorited" else "Not favorited" + toggleableState = ToggleableState(checked.value) + role = Role.Switch + }, + ) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = null // not needed here + + ) + Text("Favorite?") + } +} +// [END android_compose_accessibility_clearing] + +// [START android_compose_accessibility_hiding] +@Composable +fun WatermarkExample( + watermarkText: String, + content: @Composable () -> Unit, +) { + Box { + WatermarkedContent() + // Mark the watermark as hidden to accessibility services. + WatermarkText( + text = watermarkText, + color = Color.Gray.copy(alpha = 0.5f), + modifier = Modifier + .align(Alignment.BottomEnd) + .semantics { hideFromAccessibility() } + ) + } +} + +@Composable +fun DecorativeExample() { + Text( + modifier = + Modifier.semantics { + hideFromAccessibility() + }, + text = "A dot character that is used to decoratively separate information, like •" + ) +} +// [END android_compose_accessibility_hiding] + +@Composable +private fun WatermarkedContent() { +} + +@Composable +private fun WatermarkText( + text: String, + color: Color, + modifier: Modifier = Modifier, +) { +} + +private object ColumnWithFab { + // [START android_compose_accessibility_traversal_fab_scaffold] + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun ColumnWithFABFirstDemo() { + Scaffold( + topBar = { TopAppBar(title = { Text("Top App Bar") }) }, + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { FloatingBox() }, + content = { padding -> ContentColumn(padding = padding) }, + bottomBar = { BottomAppBar { Text("Bottom App Bar") } } + ) + } + // [END android_compose_accessibility_traversal_fab_scaffold] + + @Composable + private fun ContentColumn(padding: PaddingValues) { + } +} + +private class Post(val imageThumb: Painter? = null) +private class Metadata( + val author: Author = Author(), + val date: String? = null, + val readTimeMinutes: String? = null +) + +private class Author(val name: String = "fake") +private class BookmarkButton(isBookmarked: Boolean, onClick: () -> Boolean, modifier: Modifier) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleListDetailPaneScaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleListDetailPaneScaffold.kt new file mode 100644 index 000000000..a6be83b43 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleListDetailPaneScaffold.kt @@ -0,0 +1,281 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.adaptivelayouts + +import android.os.Parcelable +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.WindowAdaptiveInfo +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective +import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior +import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldPredictiveBackHandler +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND +import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun SampleNavigableListDetailPaneScaffoldParts() { + // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part02] + val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + // [END android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part02] + + // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part03] + NavigableListDetailPaneScaffold( + navigator = scaffoldNavigator, + // [START_EXCLUDE] + listPane = {}, + detailPane = {}, + // [END_EXCLUDE] + ) + // [END android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part03] + + // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part04] + NavigableListDetailPaneScaffold( + navigator = scaffoldNavigator, + listPane = { + AnimatedPane { + MyList( + onItemClick = { item -> + // Navigate to the detail pane with the passed item + scope.launch { + scaffoldNavigator + .navigateTo( + ListDetailPaneScaffoldRole.Detail, + item + ) + } + }, + ) + } + }, + // [START_EXCLUDE] + detailPane = {}, + // [END_EXCLUDE] + ) + // [END android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part04] + + // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part05] + NavigableListDetailPaneScaffold( + navigator = scaffoldNavigator, + // [START_EXCLUDE] + listPane = {}, + // [END_EXCLUDE] + detailPane = { + AnimatedPane { + scaffoldNavigator.currentDestination?.contentKey?.let { + MyDetails(it) + } + } + }, + ) + // [END android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_part05] +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Preview +@Composable +fun SampleNavigableListDetailPaneScaffoldFull() { + // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_full] + val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + + NavigableListDetailPaneScaffold( + navigator = scaffoldNavigator, + listPane = { + AnimatedPane { + MyList( + onItemClick = { item -> + // Navigate to the detail pane with the passed item + scope.launch { + scaffoldNavigator.navigateTo( + ListDetailPaneScaffoldRole.Detail, + item + ) + } + }, + ) + } + }, + detailPane = { + AnimatedPane { + // Show the detail pane content if selected item is available + scaffoldNavigator.currentDestination?.contentKey?.let { + MyDetails(it) + } + } + }, + ) + // [END android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_full] +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun SampleListDetailPaneScaffoldWithPredictiveBackFull() { + // [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_with_pb_full] + val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator() + val customScaffoldDirective = customPaneScaffoldDirective(currentWindowAdaptiveInfo()) + val scope = rememberCoroutineScope() + + ThreePaneScaffoldPredictiveBackHandler( + navigator = scaffoldNavigator, + backBehavior = BackNavigationBehavior.PopUntilContentChange + ) + + ListDetailPaneScaffold( + directive = customScaffoldDirective, + scaffoldState = scaffoldNavigator.scaffoldState, + listPane = { + AnimatedPane { + MyList( + onItemClick = { item -> + // Navigate to the detail pane with the passed item + scope.launch { + scaffoldNavigator.navigateTo( + ListDetailPaneScaffoldRole.Detail, + item + ) + } + }, + ) + } + }, + detailPane = { + AnimatedPane { + // Show the detail pane content if selected item is available + scaffoldNavigator.currentDestination?.contentKey?.let { + MyDetails(it) + } + } + }, + ) +} + +fun customPaneScaffoldDirective(currentWindowAdaptiveInfo: WindowAdaptiveInfo): PaneScaffoldDirective { + val horizontalPartitions = when { + currentWindowAdaptiveInfo.windowSizeClass.isWidthAtLeastBreakpoint( + WIDTH_DP_EXPANDED_LOWER_BOUND + ) -> 3 + currentWindowAdaptiveInfo.windowSizeClass.isWidthAtLeastBreakpoint( + WIDTH_DP_MEDIUM_LOWER_BOUND + ) -> 2 + else -> 1 + } + + return PaneScaffoldDirective( + maxHorizontalPartitions = horizontalPartitions, + horizontalPartitionSpacerSize = 16.dp, + maxVerticalPartitions = 1, + verticalPartitionSpacerSize = 8.dp, + defaultPanePreferredWidth = 320.dp, + excludedBounds = emptyList() + ) +} +// [END android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_with_pb_full] +@Composable +fun MyList( + onItemClick: (MyItem) -> Unit, +) { + Card { + LazyColumn { + shortStrings.forEachIndexed { id, string -> + item { + ListItem( + modifier = Modifier + .background(Color.Magenta) + .clickable { + onItemClick(MyItem(id)) + }, + headlineContent = { + Text( + text = string, + ) + }, + ) + } + } + } + } +} + +@Composable +fun MyDetails(item: MyItem) { + val text = shortStrings[item.id] + Card { + Column( + Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text( + text = "Details page for $text", + fontSize = 24.sp, + ) + Spacer(Modifier.size(16.dp)) + Text( + text = "TODO: Add great details here" + ) + } + } +} + +// [START android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_myitem] +@Parcelize +class MyItem(val id: Int) : Parcelable +// [END android_compose_adaptivelayouts_sample_list_detail_pane_scaffold_myitem] + +val shortStrings = listOf( + "Cupcake", + "Donut", + "Eclair", + "Froyo", + "Gingerbread", + "Honeycomb", + "Ice cream sandwich", + "Jelly bean", + "Kitkat", + "Lollipop", + "Marshmallow", + "Nougat", + "Oreo", + "Pie", +) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt new file mode 100644 index 000000000..6221902cf --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleNavigationSuiteScaffold.kt @@ -0,0 +1,218 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.adaptivelayouts + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItem +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND +import com.example.compose.snippets.R + +// [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_destinations] +enum class AppDestinations( + @StringRes val label: Int, + val icon: ImageVector, + @StringRes val contentDescription: Int +) { + HOME(R.string.home, Icons.Default.Home, R.string.home), + FAVORITES(R.string.favorites, Icons.Default.Favorite, R.string.favorites), + SHOPPING(R.string.shopping, Icons.Default.ShoppingCart, R.string.shopping), + PROFILE(R.string.profile, Icons.Default.AccountBox, R.string.profile), +} +// [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_destinations] + +@Composable +fun SampleNavigationSuiteScaffoldParts() { + // [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_remember] + var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) } + // [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_remember] + + // [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_items] + NavigationSuiteScaffold( + navigationSuiteItems = { + AppDestinations.entries.forEach { + item( + icon = { + Icon( + it.icon, + contentDescription = stringResource(it.contentDescription) + ) + }, + label = { Text(stringResource(it.label)) }, + selected = it == currentDestination, + onClick = { currentDestination = it } + ) + } + } + ) { + // TODO: Destination content. + } + // [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_items] + + // [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_content] + NavigationSuiteScaffold( + navigationSuiteItems = { /*...*/ } + ) { + // Destination content. + when (currentDestination) { + AppDestinations.HOME -> HomeDestination() + AppDestinations.FAVORITES -> FavoritesDestination() + AppDestinations.SHOPPING -> ShoppingDestination() + AppDestinations.PROFILE -> ProfileDestination() + } + } + // [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_content] +} + +@Composable +fun SampleNavigationSuiteScaffoldColors() { + var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) } + + // [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_container_color] + NavigationSuiteScaffold( + navigationSuiteItems = { /* ... */ }, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ) { + // Content... + } + // [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_container_color] + + // [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_suite_colors] + NavigationSuiteScaffold( + navigationSuiteItems = { /* ... */ }, + navigationSuiteColors = NavigationSuiteDefaults.colors( + navigationBarContainerColor = Color.Transparent, + ) + ) { + // Content... + } + // [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_suite_colors] + + // [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_item_colors] + val myNavigationSuiteItemColors = NavigationSuiteDefaults.itemColors( + navigationBarItemColors = NavigationBarItemDefaults.colors( + indicatorColor = MaterialTheme.colorScheme.primaryContainer, + selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + ) + + NavigationSuiteScaffold( + navigationSuiteItems = { + AppDestinations.entries.forEach { + item( + icon = { + Icon( + it.icon, + contentDescription = stringResource(it.contentDescription) + ) + }, + label = { Text(stringResource(it.label)) }, + selected = it == currentDestination, + onClick = { currentDestination = it }, + colors = myNavigationSuiteItemColors, + ) + } + }, + ) { + // Content... + } + // [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_item_colors] +} + +@Composable +fun SampleNavigationSuiteScaffoldIconsAlignment() { + var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) } + + // [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_icons_alignment] + NavigationSuiteScaffold( + navigationItems = { + AppDestinations.entries.forEach { + NavigationSuiteItem( + icon = { + Icon( + it.icon, + contentDescription = stringResource(it.contentDescription) + ) + }, + label = { Text(stringResource(it.label)) }, + selected = it == currentDestination, + onClick = { currentDestination = it }, + ) + } + }, + navigationItemVerticalArrangement = Arrangement.Center + ) { + // TODO: Destination content. + } + // [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_icons_alignment] +} + +@Composable +fun SampleNavigationSuiteScaffoldCustomType() { + // [START android_compose_adaptivelayouts_sample_navigation_suite_scaffold_layout_type] + val adaptiveInfo = currentWindowAdaptiveInfo() + val customNavSuiteType = with(adaptiveInfo) { + if (windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND)) { + NavigationSuiteType.NavigationDrawer + } else { + NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo) + } + } + + NavigationSuiteScaffold( + navigationSuiteItems = { /* ... */ }, + layoutType = customNavSuiteType, + ) { + // Content... + } + // [END android_compose_adaptivelayouts_sample_navigation_suite_scaffold_layout_type] +} + +@Composable +fun HomeDestination() {} + +@Composable +fun FavoritesDestination() {} + +@Composable +fun ShoppingDestination() {} + +@Composable +fun ProfileDestination() {} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleSupportingPaneScaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleSupportingPaneScaffold.kt new file mode 100644 index 000000000..87b6c62b7 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/adaptivelayouts/SampleSupportingPaneScaffold.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.adaptivelayouts + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContentPadding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.PaneAdaptedValue +import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold +import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole +import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope +import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior +import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldPredictiveBackHandler +import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Devices.TABLET +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun SampleNavigableSupportingPaneScaffoldParts() { + // [START android_compose_adaptivelayouts_sample_supporting_pane_scaffold_nav_and_back] + val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + + // [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_nav_and_back] + + // [START android_compose_adaptivelayouts_sample_supporting_pane_scaffold_params] + NavigableSupportingPaneScaffold( + navigator = scaffoldNavigator, + mainPane = { /*...*/ }, + supportingPane = { /*...*/ }, + ) + // [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_params] +} + +@Composable +@Preview(device = TABLET) +fun SampleNavigationSupportingPaneScaffoldFullTabletPreview() { + SampleNavigableSupportingPaneScaffoldFull() +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +@Preview +fun SampleNavigableSupportingPaneScaffoldFull() { + // [START android_compose_adaptivelayouts_sample_supporting_pane_scaffold_full] + val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange + + NavigableSupportingPaneScaffold( + navigator = scaffoldNavigator, + mainPane = { + AnimatedPane( + modifier = Modifier + .safeContentPadding() + .background(Color.Red) + ) { + if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden) { + Button( + modifier = Modifier + .wrapContentSize(), + onClick = { + scope.launch { + scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting) + } + } + ) { + Text("Show supporting pane") + } + } else { + Text("Supporting pane is shown") + } + } + }, + supportingPane = { + AnimatedPane(modifier = Modifier.safeContentPadding()) { + Column { + // Allow users to dismiss the supporting pane. Use back navigation to + // hide an expanded supporting pane. + if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) { + // Material design principles promote the usage of a right-aligned + // close (X) button. + IconButton( + modifier = Modifier.align(Alignment.End).padding(16.dp), + onClick = { + scope.launch { + scaffoldNavigator.navigateBack(backNavigationBehavior) + } + } + ) { + Icon(Icons.Default.Close, contentDescription = "Close") + } + } + Text("Supporting pane") + } + + } + } + ) + // [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_full] +} + +// [START android_compose_adaptivelayouts_sample_supporting_pane_scaffold_extracted_panes] +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun ThreePaneScaffoldPaneScope.MainPane( + shouldShowSupportingPaneButton: Boolean, + onNavigateToSupportingPane: () -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedPane( + modifier = modifier.safeContentPadding() + ) { + // Main pane content + if (shouldShowSupportingPaneButton) { + Button(onClick = onNavigateToSupportingPane) { + Text("Show supporting pane") + } + } else { + Text("Supporting pane is shown") + } + } +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun ThreePaneScaffoldPaneScope.SupportingPane( + scaffoldNavigator: ThreePaneScaffoldNavigator, + modifier: Modifier = Modifier, + backNavigationBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange, +) { + val scope = rememberCoroutineScope() + AnimatedPane(modifier = Modifier.safeContentPadding()) { + Column { + // Allow users to dismiss the supporting pane. Use back navigation to + // hide an expanded supporting pane. + if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) { + // Material design principles promote the usage of a right-aligned + // close (X) button. + IconButton( + modifier = modifier.align(Alignment.End).padding(16.dp), + onClick = { + scope.launch { + scaffoldNavigator.navigateBack(backNavigationBehavior) + } + } + ) { + Icon(Icons.Default.Close, contentDescription = "Close") + } + } + Text("Supporting pane") + } + + } +} +// [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_extracted_panes] + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun SampleNavigableSupportingPaneScaffoldSimplified() { + // [START android_compose_adaptivelayouts_sample_supporting_pane_scaffold_simplified] + val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + + NavigableSupportingPaneScaffold( + navigator = scaffoldNavigator, + mainPane = { + MainPane( + shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden, + onNavigateToSupportingPane = { + scope.launch { + scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary) + } + } + ) + }, + supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) }, + ) + // [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_simplified] +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun SampleSupportingPaneScaffoldSimplifiedWithPredictiveBackHandler() { + // [START android_compose_adaptivelayouts_sample_supporting_pane_scaffold_simplified_with_predictive_back_handler] + val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + + ThreePaneScaffoldPredictiveBackHandler( + navigator = scaffoldNavigator, + backBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange + ) + + SupportingPaneScaffold( + directive = scaffoldNavigator.scaffoldDirective, + scaffoldState = scaffoldNavigator.scaffoldState, + mainPane = { + MainPane( + shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden, + onNavigateToSupportingPane = { + scope.launch { + scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary) + } + } + ) + }, + supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) }, + ) + // [END android_compose_adaptivelayouts_sample_supporting_pane_scaffold_simplified_with_predictive_back_handler] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AdvancedAnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AdvancedAnimationSnippets.kt new file mode 100644 index 000000000..0e2ef0d75 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AdvancedAnimationSnippets.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.animations + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.calculateTargetValue +import androidx.compose.animation.splineBasedDecay +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.horizontalDrag +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.unit.IntOffset +import kotlin.math.absoluteValue +import kotlin.math.roundToInt +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +/* +* Copyright 2023 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +private object GestureAndAnimationSimple { + // [START android_compose_animations_gesture_example] + @Composable + fun Gesture() { + val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } + Box( + modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + coroutineScope { + while (true) { + // Detect a tap event and obtain its position. + awaitPointerEventScope { + val position = awaitFirstDown().position + + launch { + // Animate to the tap position. + offset.animateTo(position) + } + } + } + } + } + ) { + Circle(modifier = Modifier.offset { offset.value.toIntOffset() }) + } + } + + private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt()) + // [END android_compose_animations_gesture_example] +} + +private object GestureAndAnimationSwipeToDismiss { + // [START android_compose_animations_gesture_swipe_dismiss] + fun Modifier.swipeToDismiss( + onDismissed: () -> Unit + ): Modifier = composed { + val offsetX = remember { Animatable(0f) } + pointerInput(Unit) { + // Used to calculate fling decay. + val decay = splineBasedDecay(this) + // Use suspend functions for touch events and the Animatable. + coroutineScope { + while (true) { + val velocityTracker = VelocityTracker() + // Stop any ongoing animation. + offsetX.stop() + awaitPointerEventScope { + // Detect a touch down event. + val pointerId = awaitFirstDown().id + + horizontalDrag(pointerId) { change -> + // Update the animation value with touch events. + launch { + offsetX.snapTo( + offsetX.value + change.positionChange().x + ) + } + velocityTracker.addPosition( + change.uptimeMillis, + change.position + ) + } + } + // No longer receiving touch events. Prepare the animation. + val velocity = velocityTracker.calculateVelocity().x + val targetOffsetX = decay.calculateTargetValue( + offsetX.value, + velocity + ) + // The animation stops when it reaches the bounds. + offsetX.updateBounds( + lowerBound = -size.width.toFloat(), + upperBound = size.width.toFloat() + ) + launch { + if (targetOffsetX.absoluteValue <= size.width) { + // Not enough velocity; Slide back. + offsetX.animateTo( + targetValue = 0f, + initialVelocity = velocity + ) + } else { + // The element was swiped away. + offsetX.animateDecay(velocity, decay) + onDismissed() + } + } + } + } + } + .offset { IntOffset(offsetX.value.roundToInt(), 0) } + } + // [END android_compose_animations_gesture_swipe_dismiss] +} +@Composable +private fun Circle(modifier: Modifier = Modifier) { +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationQuickGuide.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationQuickGuide.kt new file mode 100644 index 000000000..7c6493578 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationQuickGuide.kt @@ -0,0 +1,953 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("DEPRECATION") + +package com.example.compose.snippets.animations + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.animateColor +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.EaseIn +import androidx.compose.animation.core.EaseOut +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateIntOffsetAsState +import androidx.compose.animation.core.animateRect +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextMotion +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import coil.compose.AsyncImage +import com.example.compose.snippets.R +import com.example.compose.snippets.util.randomSampleImageUrl +import java.net.URLDecoder +import java.net.URLEncoder +import kotlin.math.roundToInt +import kotlinx.coroutines.launch + +@Preview +@Composable +fun AnimationExamplesScreen() { + Column { + AnimatedVisibilityCookbook() + } +} + +@Preview +@Composable +fun AnimatedVisibilityCookbook() { + Box(modifier = Modifier.fillMaxSize()) { + // [START android_compose_animation_cookbook_visibility] + var visible by remember { + mutableStateOf(true) + } + // Animated visibility will eventually remove the item from the composition once the animation has finished. + AnimatedVisibility(visible) { + // your composable here + // [START_EXCLUDE] + Box( + modifier = Modifier + .size(200.dp) + .clip(RoundedCornerShape(8.dp)) + .background(colorGreen) + ) { + } + // [END_EXCLUDE] + } + // [END android_compose_animation_cookbook_visibility] + Button(modifier = Modifier.align(Alignment.BottomCenter), onClick = { + visible = !visible + }) { + Text("Show/Hide") + } + } +} + +@Preview +@Composable +fun AnimatedVisibilityCookbook_ModifierAlpha() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + // [START android_compose_animation_cookbook_visibility_alpha] + var visible by remember { + mutableStateOf(true) + } + val animatedAlpha by animateFloatAsState( + targetValue = if (visible) 1.0f else 0f, + label = "alpha" + ) + Box( + modifier = Modifier + .size(200.dp) + .graphicsLayer { + alpha = animatedAlpha + } + .clip(RoundedCornerShape(8.dp)) + .background(colorGreen) + .align(Alignment.TopCenter) + ) { + } + // [END android_compose_animation_cookbook_visibility_alpha] + Button(modifier = Modifier.align(Alignment.BottomCenter), onClick = { + visible = !visible + }) { + Text("Show/Hide") + } + } +} + +@Preview +@Composable +fun AnimateBackgroundColor() { + var animateBackgroundColor by remember { + mutableStateOf(true) + } + LaunchedEffect(Unit) { + animateBackgroundColor = true + } + // [START android_compose_animate_background_color] + val animatedColor by animateColorAsState( + if (animateBackgroundColor) colorGreen else colorBlue, + label = "color" + ) + Column( + modifier = Modifier.drawBehind { + drawRect(animatedColor) + } + ) { + // your composable here + } + // [END android_compose_animate_background_color] +} + +@Preview +@Composable +fun AnimatePadding() { + Box { + // [START android_compose_animation_padding] + var toggled by remember { + mutableStateOf(false) + } + val animatedPadding by animateDpAsState( + if (toggled) { + 0.dp + } else { + 20.dp + }, + label = "padding" + ) + Box( + modifier = Modifier + .aspectRatio(1f) + .fillMaxSize() + .padding(animatedPadding) + .background(Color(0xff53D9A1)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + toggled = !toggled + } + ) + // [END android_compose_animation_padding] + } +} + +@Preview +@Composable +fun AnimateSizeChange() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + // [START android_compose_animation_size_change] + var expanded by remember { mutableStateOf(false) } + Box( + modifier = Modifier + .background(colorBlue) + .animateContentSize() + .height(if (expanded) 400.dp else 200.dp) + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + expanded = !expanded + } + + ) { + } + // [END android_compose_animation_size_change] + } +} + +@Preview +@Composable +fun AnimateOffset() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + // [START android_compose_animation_offset_change] + var moved by remember { mutableStateOf(false) } + val pxToMove = with(LocalDensity.current) { + 100.dp.toPx().roundToInt() + } + val offset by animateIntOffsetAsState( + targetValue = if (moved) { + IntOffset(pxToMove, pxToMove) + } else { + IntOffset.Zero + }, + label = "offset" + ) + + Box( + modifier = Modifier + .offset { + offset + } + .background(colorBlue) + .size(100.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + moved = !moved + } + ) + // [END android_compose_animation_offset_change] + } +} + +@Preview +@Composable +fun AnimateBetweenComposableDestinations() { + // [START android_compose_animate_destinations] + val navController = rememberNavController() + NavHost( + navController = navController, startDestination = "landing", + enterTransition = { EnterTransition.None }, + exitTransition = { ExitTransition.None } + ) { + composable("landing") { + ScreenLanding( + // [START_EXCLUDE] + onItemClicked = { + navController.navigate("detail/${URLEncoder.encode(it)}") + } + // [END_EXCLUDE] + ) + } + composable( + "detail/{photoUrl}", + arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }), + enterTransition = { + fadeIn( + animationSpec = tween( + 300, easing = LinearEasing + ) + ) + slideIntoContainer( + animationSpec = tween(300, easing = EaseIn), + towards = AnimatedContentTransitionScope.SlideDirection.Start + ) + }, + exitTransition = { + fadeOut( + animationSpec = tween( + 300, easing = LinearEasing + ) + ) + slideOutOfContainer( + animationSpec = tween(300, easing = EaseOut), + towards = AnimatedContentTransitionScope.SlideDirection.End + ) + } + ) { backStackEntry -> + ScreenDetails( + // [START_EXCLUDE] + photo = URLDecoder.decode(backStackEntry.arguments!!.getString("photoUrl")!!), + onBackClicked = { + navController.popBackStack() + } + // [END_EXCLUDE] + ) + } + } + // [END android_compose_animate_destinations] +} + +@Preview +@Composable +fun AnimateSizeChange_Specs() { + Row(modifier = Modifier.fillMaxSize()) { + var expanded by remember { mutableStateOf(false) } + Column( + modifier = Modifier + .padding(8.dp) + .weight(1f) + ) { + Text("No spec set") + Box( + modifier = Modifier + .background(colorBlue) + .animateContentSize() + .height(if (expanded) 300.dp else 200.dp) + .fillMaxSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + expanded = !expanded + } + + ) { + } + } + Column( + modifier = Modifier + .padding(8.dp) + .weight(1f) + ) { + Text("Custom spec") + // [START android_compose_animation_size_change_spec] + Box( + modifier = Modifier + .background(colorBlue) + .animateContentSize( + spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioHighBouncy + ) + ) + .height(if (expanded) 300.dp else 200.dp) + .fillMaxSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + expanded = !expanded + } + + ) { + } + // [END android_compose_animation_size_change_spec] + } + } +} + +@Preview +@Composable +fun SmoothAnimateText() { + // [START android_compose_animation_cookbook_text] + val infiniteTransition = rememberInfiniteTransition(label = "infinite transition") + val scale by infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 8f, + animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse), + label = "scale" + ) + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = "Hello", + modifier = Modifier + .graphicsLayer { + scaleX = scale + scaleY = scale + transformOrigin = TransformOrigin.Center + } + .align(Alignment.Center), + // Text composable does not take TextMotion as a parameter. + // Provide it via style argument but make sure that we are copying from current theme + style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated) + ) + } + + // [END android_compose_animation_cookbook_text] +} + +@Preview +@Composable +fun AnimateTextColor() { + Box(modifier = Modifier.fillMaxSize()) { + // [START android_compose_animation_cookbook_text_color] + val infiniteTransition = rememberInfiniteTransition(label = "infinite transition") + val animatedColor by infiniteTransition.animateColor( + initialValue = Color(0xFF60DDAD), + targetValue = Color(0xFF4285F4), + animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse), + label = "color" + ) + + BasicText( + text = "Hello Compose", + color = { + animatedColor + }, + // [START_EXCLUDE] + style = MaterialTheme.typography.displayLarge, + modifier = Modifier.align(Alignment.Center).padding(16.dp) + // [END_EXCLUDE] + ) + // [END android_compose_animation_cookbook_text_color] + } +} + +@Preview +@Composable +fun InfinitelyRepeatable() { + // [START android_compose_animation_infinitely_repeating] + val infiniteTransition = rememberInfiniteTransition(label = "infinite") + val color by infiniteTransition.animateColor( + initialValue = Color.Green, + targetValue = Color.Blue, + animationSpec = infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "color" + ) + Column( + modifier = Modifier.drawBehind { + drawRect(color) + } + ) { + // your composable here + } + // [END android_compose_animation_infinitely_repeating] +} + +@Preview +@Composable +fun ConcurrentAnimatable() { + // [START android_compose_animation_on_launch] + val alphaAnimation = remember { + Animatable(0f) + } + LaunchedEffect(Unit) { + alphaAnimation.animateTo(1f) + } + Box( + modifier = Modifier.graphicsLayer { + alpha = alphaAnimation.value + } + ) + // [END android_compose_animation_on_launch] +} + +@Preview +@Composable +fun SequentialAnimations() { + // [START android_compose_animation_sequential] + val alphaAnimation = remember { Animatable(0f) } + val yAnimation = remember { Animatable(0f) } + + LaunchedEffect("animationKey") { + alphaAnimation.animateTo(1f) + yAnimation.animateTo(100f) + yAnimation.animateTo(500f, animationSpec = tween(100)) + } + // [END android_compose_animation_sequential] +} + +@Preview +@Composable +fun ConcurrentAnimations() { + // [START android_compose_animation_concurrent] + val alphaAnimation = remember { Animatable(0f) } + val yAnimation = remember { Animatable(0f) } + + LaunchedEffect("animationKey") { + launch { + alphaAnimation.animateTo(1f) + } + launch { + yAnimation.animateTo(100f) + } + } + // [END android_compose_animation_concurrent] +} +enum class BoxState { + Collapsed, + Expanded +} +@Preview +@Composable +fun TransitionExampleConcurrent() { + // [START android_compose_concurrent_transition] + var currentState by remember { mutableStateOf(BoxState.Collapsed) } + val transition = updateTransition(currentState, label = "transition") + + val rect by transition.animateRect(label = "rect") { state -> + when (state) { + BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f) + BoxState.Expanded -> Rect(100f, 100f, 300f, 300f) + } + } + val borderWidth by transition.animateDp(label = "borderWidth") { state -> + when (state) { + BoxState.Collapsed -> 1.dp + BoxState.Expanded -> 0.dp + } + } + // [END android_compose_concurrent_transition] +} + +@Preview +@Composable +fun AnimateElevation() { + Box( + modifier = Modifier + .fillMaxSize() + ) { + // [START android_compose_animation_cookbook_elevation] + val mutableInteractionSource = remember { + MutableInteractionSource() + } + val pressed = mutableInteractionSource.collectIsPressedAsState() + val elevation = animateDpAsState( + targetValue = if (pressed.value) { + 32.dp + } else { + 8.dp + }, + label = "elevation" + ) + Box( + modifier = Modifier + .size(100.dp) + .align(Alignment.Center) + .graphicsLayer { + this.shadowElevation = elevation.value.toPx() + } + .clickable(interactionSource = mutableInteractionSource, indication = null) { + } + .background(colorGreen) + ) { + } + // [END android_compose_animation_cookbook_elevation] + } +} + +@Preview +@Composable +fun AnimatedContentExampleSwitch() { + // [START android_compose_animation_cookbook_animated_content] + var state by remember { + mutableStateOf(UiState.Loading) + } + AnimatedContent( + state, + transitionSpec = { + fadeIn( + animationSpec = tween(3000) + ) togetherWith fadeOut(animationSpec = tween(3000)) + }, + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + state = when (state) { + UiState.Loading -> UiState.Loaded + UiState.Loaded -> UiState.Error + UiState.Error -> UiState.Loading + } + }, + label = "Animated Content" + ) { targetState -> + when (targetState) { + UiState.Loading -> { + LoadingScreen() + } + UiState.Loaded -> { + LoadedScreen() + } + UiState.Error -> { + ErrorScreen() + } + } + } + // [END android_compose_animation_cookbook_animated_content] +} + +@Composable +private fun ErrorScreen() { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // [START_EXCLUDE] + Text("Error", fontSize = 18.sp) + // [END_EXCLUDE] + } +} + +@Composable +private fun LoadedScreen() { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // [START_EXCLUDE] + Text("Loaded", fontSize = 18.sp) + Image( + painterResource(id = R.drawable.dog), + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .padding(16.dp) + .clip( + RoundedCornerShape(16.dp) + ), + contentDescription = "dog", + contentScale = ContentScale.Crop + ) + // [END_EXCLUDE] + } +} + +@Composable +private fun LoadingScreen() { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + + ) { + CircularProgressIndicator(modifier = Modifier.size(48.dp)) + Spacer(modifier = Modifier.height(16.dp)) + Text("Loading", fontSize = 18.sp) + } +} + +@Preview +@Composable +fun AnimationLayout() { + // [START android_compose_animation_layout_offset] + var toggled by remember { + mutableStateOf(false) + } + val interactionSource = remember { + MutableInteractionSource() + } + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize() + .clickable(indication = null, interactionSource = interactionSource) { + toggled = !toggled + } + ) { + val offsetTarget = if (toggled) { + IntOffset(150, 150) + } else { + IntOffset.Zero + } + val offset = animateIntOffsetAsState( + targetValue = offsetTarget, label = "offset" + ) + Box( + modifier = Modifier + .size(100.dp) + .background(colorBlue) + ) + Box( + modifier = Modifier + .layout { measurable, constraints -> + val offsetValue = if (isLookingAhead) offsetTarget else offset.value + val placeable = measurable.measure(constraints) + layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) { + placeable.placeRelative(offsetValue) + } + } + .size(100.dp) + .background(colorGreen) + ) + Box( + modifier = Modifier + .size(100.dp) + .background(colorBlue) + ) + } + // [END android_compose_animation_layout_offset] +} + +@Preview +@Composable +fun AnimateAlignment() { + // [START android_compose_animate_item_placement] + var toggled by remember { + mutableStateOf(false) + } + val interactionSource = remember { + MutableInteractionSource() + } + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize() + .clickable(indication = null, interactionSource = interactionSource) { + toggled = !toggled + } + ) { + + Box( + modifier = Modifier + .size(100.dp) + .background(colorBlue) + ) + Box( + modifier = Modifier + .size(100.dp) + .background(colorGreen) + ) + Box( + modifier = Modifier + .size(100.dp) + .background(colorBlue) + ) + } + // [END android_compose_animate_item_placement] +} + +enum class UiState { + Loading, + Loaded, + Error +} + +val colorGreen = Color(0xFF53D9A1) +val colorBlue = Color(0xFF4FC3F7) + +@Preview +@Composable +fun AnimationLayoutIndividualItem() { + var toggled by remember { + mutableStateOf(false) + } + val interactionSource = remember { + MutableInteractionSource() + } + Column( + modifier = Modifier + .fillMaxSize() + .clickable(indication = null, interactionSource = interactionSource) { + toggled = !toggled + } + ) { + val offset = animateIntOffsetAsState( + targetValue = if (toggled) { + IntOffset(150, 150) + } else { + IntOffset.Zero + }, + label = "offset" + ) + Box( + modifier = Modifier + .size(100.dp) + .background(colorBlue) + ) + Box( + modifier = Modifier + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width + offset.value.x, placeable.height + offset.value.y) { + placeable.placeRelative(offset.value) + } + } + .size(100.dp) + .background(colorGreen) + ) + Box( + modifier = Modifier + .size(100.dp) + .background(colorBlue) + ) + } +} + +@Composable +private fun ScreenLanding(onItemClicked: (String) -> Unit) { + LazyVerticalGrid( + columns = GridCells.Adaptive(200.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + content = { + items(randomSizedPhotos) { photo -> + AsyncImage( + model = photo, + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .clickable { + onItemClicked(photo) + } + ) + } + }, + modifier = Modifier.fillMaxSize() + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ScreenDetails(photo: String, onBackClicked: () -> Unit) { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { + Text("Photo Details") + }, + navigationIcon = { + IconButton(onClick = { onBackClicked() }) { + Icon(Icons.Default.ArrowBack, contentDescription = "Back") + } + } + ) + } + ) { padding -> + Column(modifier = Modifier.padding(padding)) { + AsyncImage( + model = photo, + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + ) + Spacer(modifier = Modifier.height(18.dp)) + Text("Photo details", fontSize = 18.sp, modifier = Modifier.padding(8.dp)) + } + } +} + +private val randomSizedPhotos = listOf( + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 900, height = 1600), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 900, height = 1600), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 900, height = 1600), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 900, height = 1600), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), +) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt new file mode 100644 index 000000000..bdb82790d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt @@ -0,0 +1,972 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("CanBeVal", "UNUSED_VARIABLE", "UNUSED_PARAMETER", "unused") + +package com.example.compose.snippets.animations + +import androidx.compose.animation.Animatable +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.EnterExitState +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.animateColor +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.AnimationVector2D +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.ExperimentalAnimationSpecApi +import androidx.compose.animation.core.ExperimentalTransitionApi +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.TargetBasedAnimation +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.TwoWayConverter +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateOffsetAsState +import androidx.compose.animation.core.animateRect +import androidx.compose.animation.core.animateValueAsState +import androidx.compose.animation.core.createChildTransition +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.keyframesWithSpline +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.rememberTransition +import androidx.compose.animation.core.repeatable +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Phone +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.PointMode +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.round +import com.example.compose.snippets.R +import java.text.BreakIterator +import java.text.StringCharacterIterator +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive + +/* +* Copyright 2023 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +@Preview +@Composable +private fun AnimatedVisibilitySample() { + // [START android_compose_animations_animated_visibility] + var editable by remember { mutableStateOf(true) } + AnimatedVisibility(visible = editable) { + Text(text = "Edit") + } + // [END android_compose_animations_animated_visibility] +} + +@Preview +@Composable +private fun AnimatedVisibilityWithEnterAndExit() { + // [START android_compose_animations_animated_visibility_enter_exit] + var visible by remember { mutableStateOf(true) } + val density = LocalDensity.current + AnimatedVisibility( + visible = visible, + enter = slideInVertically { + // Slide in from 40 dp from the top. + with(density) { -40.dp.roundToPx() } + } + expandVertically( + // Expand from the top. + expandFrom = Alignment.Top + ) + fadeIn( + // Fade in with the initial alpha of 0.3f. + initialAlpha = 0.3f + ), + exit = slideOutVertically() + shrinkVertically() + fadeOut() + ) { + Text( + "Hello", + Modifier + .fillMaxWidth() + .height(200.dp) + ) + } + // [END android_compose_animations_animated_visibility_enter_exit] +} + +@Preview +@Composable +private fun AnimatedVisibilityMutable() { + // [START android_compose_animations_animated_visibility_mutable] + // Create a MutableTransitionState for the AnimatedVisibility. + val state = remember { + MutableTransitionState(false).apply { + // Start the animation immediately. + targetState = true + } + } + Column { + AnimatedVisibility(visibleState = state) { + Text(text = "Hello, world!") + } + + // Use the MutableTransitionState to know the current animation state + // of the AnimatedVisibility. + Text( + text = when { + state.isIdle && state.currentState -> "Visible" + !state.isIdle && state.currentState -> "Disappearing" + state.isIdle && !state.currentState -> "Invisible" + else -> "Appearing" + } + ) + } + // [END android_compose_animations_animated_visibility_mutable] +} + +@Composable +@Preview +private fun AnimatedVisibilityAnimateEnterExitChildren() { + // [START android_compose_animations_animated_visibility_animate_enter_exit_children] + var visible by remember { mutableStateOf(true) } + + AnimatedVisibility( + visible = visible, + enter = fadeIn(), + exit = fadeOut() + ) { + // Fade in/out the background and the foreground. + Box( + Modifier + .fillMaxSize() + .background(Color.DarkGray) + ) { + Box( + Modifier + .align(Alignment.Center) + .animateEnterExit( + // Slide in/out the inner box. + enter = slideInVertically(), + exit = slideOutVertically() + ) + .sizeIn(minWidth = 256.dp, minHeight = 64.dp) + .background(Color.Red) + ) { + // Content of the notification… + } + } + } + // [END android_compose_animations_animated_visibility_animate_enter_exit_children] +} + +@Preview +@Composable +private fun AnimatedVisibilityTransition() { + // [START android_compose_animations_animated_visibility_transition] + var visible by remember { mutableStateOf(true) } + + AnimatedVisibility( + visible = visible, + enter = fadeIn(), + exit = fadeOut() + ) { // this: AnimatedVisibilityScope + // Use AnimatedVisibilityScope#transition to add a custom animation + // to the AnimatedVisibility. + val background by transition.animateColor(label = "color") { state -> + if (state == EnterExitState.Visible) Color.Blue else Color.Gray + } + Box( + modifier = Modifier + .size(128.dp) + .background(background) + ) + } + // [END android_compose_animations_animated_visibility_transition] +} + +@Composable +@Preview +private fun AnimateAsStateSimple() { + // [START android_compose_animations_animate_as_state] + var enabled by remember { mutableStateOf(true) } + + val animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha") + Box( + Modifier + .fillMaxSize() + .graphicsLayer { alpha = animatedAlpha } + .background(Color.Red) + ) + // [END android_compose_animations_animate_as_state] + { Button(onClick = { enabled = !enabled }) { Text("Animate me!") } } +} + +@Preview +@Composable +private fun AnimatedContentSimple() { + // [START android_compose_animations_animated_content_simple] + Row { + var count by remember { mutableIntStateOf(0) } + Button(onClick = { count++ }) { + Text("Add") + } + AnimatedContent( + targetState = count, + label = "animated content" + ) { targetCount -> + // Make sure to use `targetCount`, not `count`. + Text(text = "Count: $targetCount") + } + } + // [END android_compose_animations_animated_content_simple] +} + +@Composable +private fun AnimatedContentTransitionSpec(count: Int) { + // [START android_compose_animations_animated_content_transition_spec] + AnimatedContent( + targetState = count, + transitionSpec = { + // Compare the incoming number with the previous number. + if (targetState > initialState) { + // If the target number is larger, it slides up and fades in + // while the initial (smaller) number slides up and fades out. + slideInVertically { height -> height } + fadeIn() togetherWith + slideOutVertically { height -> -height } + fadeOut() + } else { + // If the target number is smaller, it slides down and fades in + // while the initial number slides down and fades out. + slideInVertically { height -> -height } + fadeIn() togetherWith + slideOutVertically { height -> height } + fadeOut() + }.using( + // Disable clipping since the faded slide-in/out should + // be displayed out of bounds. + SizeTransform(clip = false) + ) + }, label = "animated content" + ) { targetCount -> + Text(text = "$targetCount") + } + // [END android_compose_animations_animated_content_transition_spec] +} + +@Composable +private fun AnimatedContentSizeTransform() { + // [START android_compose_animations_animated_content_size_transform] + var expanded by remember { mutableStateOf(false) } + Surface( + color = MaterialTheme.colorScheme.primary, + onClick = { expanded = !expanded } + ) { + AnimatedContent( + targetState = expanded, + transitionSpec = { + fadeIn(animationSpec = tween(150, 150)) togetherWith + fadeOut(animationSpec = tween(150)) using + SizeTransform { initialSize, targetSize -> + if (targetState) { + keyframes { + // Expand horizontally first. + IntSize(targetSize.width, initialSize.height) at 150 + durationMillis = 300 + } + } else { + keyframes { + // Shrink vertically first. + IntSize(initialSize.width, targetSize.height) at 150 + durationMillis = 300 + } + } + } + }, label = "size transform" + ) { targetExpanded -> + if (targetExpanded) { + Expanded() + } else { + ContentIcon() + } + } + } + // [END android_compose_animations_animated_content_size_transform] +} + +@Composable +private fun AnimateContentSizeSimple() { + // [START android_compose_animations_animated_content_size_modifier_simple] + var message by remember { mutableStateOf("Hello") } + Box( + modifier = Modifier + .background(Color.Blue) + .animateContentSize() + ) { Text(text = message) } + // [END android_compose_animations_animated_content_size_modifier_simple] +} + +@Composable +private fun CrossfadeSimple() { + // [START android_compose_animations_crossfade_simple] + var currentPage by remember { mutableStateOf("A") } + Crossfade(targetState = currentPage, label = "cross fade") { screen -> + when (screen) { + "A" -> Text("Page A") + "B" -> Text("Page B") + } + } + // [END android_compose_animations_crossfade_simple] +} + +private object UpdateTransitionEnumState { + // [START android_compose_animations_transitions_box_state] + enum class BoxState { + Collapsed, + Expanded + } + // [END android_compose_animations_transitions_box_state] + + @Composable + private fun UpdateTransitionInstance() { + // [START android_compose_animations_transitions_instance] + var currentState by remember { mutableStateOf(BoxState.Collapsed) } + val transition = updateTransition(currentState, label = "box state") + // [END android_compose_animations_transitions_instance] + } + + @Composable + private fun UpdateTransitionAnimationValues(transition: Transition) { + // [START android_compose_animations_transitions_values] + val rect by transition.animateRect(label = "rectangle") { state -> + when (state) { + BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f) + BoxState.Expanded -> Rect(100f, 100f, 300f, 300f) + } + } + val borderWidth by transition.animateDp(label = "border width") { state -> + when (state) { + BoxState.Collapsed -> 1.dp + BoxState.Expanded -> 0.dp + } + } + // [END android_compose_animations_transitions_values] + } + + @Composable + private fun UpdateTransitionTransitionSpec(transition: Transition) { + // [START android_compose_animations_transitions_spec] + val color by transition.animateColor( + transitionSpec = { + when { + BoxState.Expanded isTransitioningTo BoxState.Collapsed -> + spring(stiffness = 50f) + + else -> + tween(durationMillis = 500) + } + }, label = "color" + ) { state -> + when (state) { + BoxState.Collapsed -> MaterialTheme.colorScheme.primary + BoxState.Expanded -> MaterialTheme.colorScheme.background + } + } + // [END android_compose_animations_transitions_spec] + } + + @Composable + private fun UpdateTransitionMutableTransitionState() { + // [START android_compose_animations_transitions_state] + // Start in collapsed state and immediately animate to expanded + var currentState = remember { MutableTransitionState(BoxState.Collapsed) } + currentState.targetState = BoxState.Expanded + val transition = rememberTransition(currentState, label = "box state") + // …… + // [END android_compose_animations_transitions_state] + } +} + +@OptIn(ExperimentalTransitionApi::class) +private object UpdateTransitionCreateChildTransition { + + // [START android_compose_animations_transitions_dialer_example] + enum class DialerState { DialerMinimized, NumberPad } + + @Composable + fun DialerButton(isVisibleTransition: Transition) { + // `isVisibleTransition` spares the need for the content to know + // about other DialerStates. Instead, the content can focus on + // animating the state change between visible and not visible. + } + + @Composable + fun NumberPad(isVisibleTransition: Transition) { + // `isVisibleTransition` spares the need for the content to know + // about other DialerStates. Instead, the content can focus on + // animating the state change between visible and not visible. + } + + @Composable + fun Dialer(dialerState: DialerState) { + val transition = updateTransition(dialerState, label = "dialer state") + Box { + // Creates separate child transitions of Boolean type for NumberPad + // and DialerButton for any content animation between visible and + // not visible + NumberPad( + transition.createChildTransition { + it == DialerState.NumberPad + } + ) + DialerButton( + transition.createChildTransition { + it == DialerState.DialerMinimized + } + ) + } + } + // [END android_compose_animations_transitions_dialer_example] +} + +@Composable +private fun UpdateTransitionAnimatedVisibility() { + // [START android_compose_animations_transitions_animated_visibility] + var selected by remember { mutableStateOf(false) } + // Animates changes when `selected` is changed. + val transition = updateTransition(selected, label = "selected state") + val borderColor by transition.animateColor(label = "border color") { isSelected -> + if (isSelected) Color.Magenta else Color.White + } + val elevation by transition.animateDp(label = "elevation") { isSelected -> + if (isSelected) 10.dp else 2.dp + } + Surface( + onClick = { selected = !selected }, + shape = RoundedCornerShape(8.dp), + border = BorderStroke(2.dp, borderColor), + shadowElevation = elevation + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text(text = "Hello, world!") + // AnimatedVisibility as a part of the transition. + transition.AnimatedVisibility( + visible = { targetSelected -> targetSelected }, + enter = expandVertically(), + exit = shrinkVertically() + ) { + Text(text = "It is fine today.") + } + // AnimatedContent as a part of the transition. + transition.AnimatedContent { targetState -> + if (targetState) { + Text(text = "Selected") + } else { + Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone") + } + } + } + } + // [END android_compose_animations_transitions_animated_visibility] +} + +private object UpdateTransitionEncapsulating { + // [START android_compose_animations_transitions_encapsulating] + enum class BoxState { Collapsed, Expanded } + + @Composable + fun AnimatingBox(boxState: BoxState) { + val transitionData = updateTransitionData(boxState) + // UI tree + Box( + modifier = Modifier + .background(transitionData.color) + .size(transitionData.size) + ) + } + + // Holds the animation values. + private class TransitionData( + color: State, + size: State + ) { + val color by color + val size by size + } + + // Create a Transition and return its animation values. + @Composable + private fun updateTransitionData(boxState: BoxState): TransitionData { + val transition = updateTransition(boxState, label = "box state") + val color = transition.animateColor(label = "color") { state -> + when (state) { + BoxState.Collapsed -> Color.Gray + BoxState.Expanded -> Color.Red + } + } + val size = transition.animateDp(label = "size") { state -> + when (state) { + BoxState.Collapsed -> 64.dp + BoxState.Expanded -> 128.dp + } + } + return remember(transition) { TransitionData(color, size) } + } + // [END android_compose_animations_transitions_encapsulating] +} + +@Composable +private fun RememberInfiniteTransitionSimple() { + // [START android_compose_animations_infinite_transition_simple] + val infiniteTransition = rememberInfiniteTransition(label = "infinite") + val color by infiniteTransition.animateColor( + initialValue = Color.Red, + targetValue = Color.Green, + animationSpec = infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "color" + ) + + Box( + Modifier + .fillMaxSize() + .background(color) + ) + // [END android_compose_animations_infinite_transition_simple] +} + +@Composable +private fun AnimatableSimple(ok: Boolean) { + // [START android_compose_animations_animatable_simple] + // Start out gray and animate to green/red based on `ok` + val color = remember { Animatable(Color.Gray) } + LaunchedEffect(ok) { + color.animateTo(if (ok) Color.Green else Color.Red) + } + Box( + Modifier + .fillMaxSize() + .background(color.value) + ) + // [END android_compose_animations_animatable_simple] +} + +@Composable +private fun TargetBasedAnimationSimple(someCustomCondition: () -> Boolean) { + // [START android_compose_animations_target_based_animation_simple] + val anim = remember { + TargetBasedAnimation( + animationSpec = tween(200), + typeConverter = Float.VectorConverter, + initialValue = 200f, + targetValue = 1000f + ) + } + var playTime by remember { mutableLongStateOf(0L) } + + LaunchedEffect(anim) { + val startTime = withFrameNanos { it } + + do { + playTime = withFrameNanos { it } - startTime + val animationValue = anim.getValueFromNanos(playTime) + } while (someCustomCondition()) + } + // [END android_compose_animations_target_based_animation_simple] +} + +@Composable +private fun AnimationSpecTween(enabled: Boolean) { + // [START android_compose_animations_spec_tween] + val alpha: Float by animateFloatAsState( + targetValue = if (enabled) 1f else 0.5f, + // Configure the animation duration and easing. + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), + label = "alpha" + ) + // [END android_compose_animations_spec_tween] +} + +@Composable +private fun AnimationSpecSpring() { + // [START android_compose_animations_spec_spring] + val value by animateFloatAsState( + targetValue = 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioHighBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "spring spec" + ) + // [END android_compose_animations_spec_spring] +} + +@Composable +private fun AnimationSpecTween() { + // [START android_compose_animations_spec_tween_delay] + val value by animateFloatAsState( + targetValue = 1f, + animationSpec = tween( + durationMillis = 300, + delayMillis = 50, + easing = LinearOutSlowInEasing + ), + label = "tween delay" + ) + // [END android_compose_animations_spec_tween_delay] +} + +@Composable +private fun AnimationSpecKeyframe() { + // [START android_compose_animations_spec_keyframe] + val value by animateFloatAsState( + targetValue = 1f, + animationSpec = keyframes { + durationMillis = 375 + 0.0f at 0 using LinearOutSlowInEasing // for 0-15 ms + 0.2f at 15 using FastOutLinearInEasing // for 15-75 ms + 0.4f at 75 // ms + 0.4f at 225 // ms + }, + label = "keyframe" + ) + // [END android_compose_animations_spec_keyframe] +} + +@OptIn(ExperimentalAnimationSpecApi::class) +@Composable +private fun AnimationSpecKeyframeWithSpline() { + // [START android_compose_animation_spec_keyframes_with_spline] + val offset by animateOffsetAsState( + targetValue = Offset(300f, 300f), + animationSpec = keyframesWithSpline { + durationMillis = 6000 + Offset(0f, 0f) at 0 + Offset(150f, 200f) atFraction 0.5f + Offset(0f, 100f) atFraction 0.7f + } + ) + // [END android_compose_animation_spec_keyframes_with_spline] +} + +@OptIn(ExperimentalAnimationSpecApi::class) +@Preview +@Composable +private fun OffsetKeyframeWithSplineDemo() { + val points = remember { mutableStateListOf() } + val offsetAnim = remember { + Animatable( + Offset.Zero, + Offset.VectorConverter + ) + } + val density = LocalDensity.current + + BoxWithConstraints( + Modifier.fillMaxSize().drawBehind { + drawPoints( + points = points, + pointMode = PointMode.Lines, + color = Color.LightGray, + strokeWidth = 4f, + pathEffect = PathEffect.dashPathEffect(floatArrayOf(30f, 20f)) + ) + } + ) { + val minDimension = minOf(maxWidth, maxHeight) + val size = minDimension / 4 + + val sizePx = with(density) { size.toPx() } + val widthPx = with(density) { maxWidth.toPx() } + val heightPx = with(density) { maxHeight.toPx() } + + val maxXOff = (widthPx - sizePx) / 2f + val maxYOff = heightPx - (sizePx / 2f) + + Box( + Modifier.align(Alignment.TopCenter) + .offset { offsetAnim.value.round() } + .size(size) + .background(Color.Red, RoundedCornerShape(50)) + .onPlaced { points.add(it.boundsInParent().center) } + ) + + LaunchedEffect(Unit) { + delay(1000) + while (isActive) { + offsetAnim.animateTo( + targetValue = Offset.Zero, + animationSpec = + keyframesWithSpline { + durationMillis = 4400 + + // Increasingly approach the halfway point moving from side to side + for (i in 0..4) { + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (i.toFloat() / 5f) * sign, + y = (maxYOff) * (i.toFloat() / 5f) + ) atFraction (0.1f * i) + } + + // Halfway point (at bottom of the screen) + Offset(0f, maxYOff) atFraction 0.5f + + // Return with mirrored movement + for (i in 0..4) { + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (1f - i.toFloat() / 5f) * sign, + y = (maxYOff) * (1f - i.toFloat() / 5f) + ) atFraction ((0.1f * i) + 0.5f) + } + } + ) + points.clear() + } + } + } +} + +@Composable +private fun AnimationSpecRepeatable() { + // [START android_compose_animations_spec_repeatable] + val value by animateFloatAsState( + targetValue = 1f, + animationSpec = repeatable( + iterations = 3, + animation = tween(durationMillis = 300), + repeatMode = RepeatMode.Reverse + ), + label = "repeatable spec" + ) + // [END android_compose_animations_spec_repeatable] +} + +@Composable +private fun AnimationSpecInfiniteRepeatable() { + // [START android_compose_animations_spec_infinite_repeatable] + val value by animateFloatAsState( + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 300), + repeatMode = RepeatMode.Reverse + ), + label = "infinite repeatable" + ) + // [END android_compose_animations_spec_infinite_repeatable] +} + +@Composable +private fun AnimationSpecSnap() { + // [START android_compose_animations_spec_snap] + val value by animateFloatAsState( + targetValue = 1f, + animationSpec = snap(delayMillis = 50), + label = "snap spec" + ) + // [END android_compose_animations_spec_snap] +} + +private object Easing { + // [START android_compose_animations_easing_usage] + val CustomEasing = Easing { fraction -> fraction * fraction } + + @Composable + fun EasingUsage() { + val value by animateFloatAsState( + targetValue = 1f, + animationSpec = tween( + durationMillis = 300, + easing = CustomEasing + ), + label = "custom easing" + ) + // …… + } + // [END android_compose_animations_easing_usage] +} + +private object AnimationVectorTwoWayConverter { + // [START android_compose_animations_vector_convertor] + val IntToVector: TwoWayConverter = + TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() }) + // [END android_compose_animations_vector_convertor] +} + +private object AnimationVectorCustomType { + // [START android_compose_animations_vector_convertor_custom_type] + data class MySize(val width: Dp, val height: Dp) + + @Composable + fun MyAnimation(targetSize: MySize) { + val animSize: MySize by animateValueAsState( + targetSize, + TwoWayConverter( + convertToVector = { size: MySize -> + // Extract a float value from each of the `Dp` fields. + AnimationVector2D(size.width.value, size.height.value) + }, + convertFromVector = { vector: AnimationVector2D -> + MySize(vector.v1.dp, vector.v2.dp) + } + ), + label = "size" + ) + } + // [END android_compose_animations_vector_convertor_custom_type] +} + +@OptIn(ExperimentalAnimationGraphicsApi::class) +@Preview +// [START android_compose_animations_vector_drawable] +@Composable +fun AnimatedVectorDrawable() { + val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated) + var atEnd by remember { mutableStateOf(false) } + Image( + painter = rememberAnimatedVectorPainter(image, atEnd), + contentDescription = "Timer", + modifier = Modifier.clickable { + atEnd = !atEnd + }, + contentScale = ContentScale.Crop + ) +} +// [END android_compose_animations_vector_drawable] + +@Composable +private fun Expanded() { +} + +@Composable +private fun ContentIcon() { +} + +// [START android_compose_animations_char_by_char] +@Composable +private fun AnimatedText() { + val text = "This text animates as though it is being typed \uD83E\uDDDE\u200D♀\uFE0F \uD83D\uDD10 \uD83D\uDC69\u200D❤\uFE0F\u200D\uD83D\uDC68 \uD83D\uDC74\uD83C\uDFFD" + + // Use BreakIterator as it correctly iterates over characters regardless of how they are + // stored, for example, some emojis are made up of multiple characters. + // You don't want to break up an emoji as it animates, so using BreakIterator will ensure + // this is correctly handled! + val breakIterator = remember(text) { BreakIterator.getCharacterInstance() } + + // Define how many milliseconds between each character should pause for. This will create the + // illusion of an animation, as we delay the job after each character is iterated on. + val typingDelayInMs = 50L + + var substringText by remember { + mutableStateOf("") + } + LaunchedEffect(text) { + // Initial start delay of the typing animation + delay(1000) + breakIterator.text = StringCharacterIterator(text) + + var nextIndex = breakIterator.next() + // Iterate over the string, by index boundary + while (nextIndex != BreakIterator.DONE) { + substringText = text.subSequence(0, nextIndex).toString() + // Go to the next logical character boundary + nextIndex = breakIterator.next() + delay(typingDelayInMs) + } + } + Text(substringText) +// [END android_compose_animations_char_by_char] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/Cheese.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/Cheese.kt new file mode 100644 index 000000000..2468bce9f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/Cheese.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo + +import androidx.annotation.DrawableRes +import com.example.compose.snippets.R + +val CheeseImages = listOf( + R.drawable.cheese_1, + R.drawable.cheese_2, + R.drawable.cheese_3, + R.drawable.cheese_4, + R.drawable.cheese_5 +) + +val CheeseNames = listOf( + "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi", + "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", + "Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese", + "Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell", + "Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc", + "Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss", + "Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon", + "Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase", + "Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese", + "Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop Kennedy", + "Blarney", "Bleu d'Auvergne", "Bleu de Gex", "Bleu de Laqueuille", + "Bleu de Septmoncel", "Bleu Des Causses", "Blue", "Blue Castello", "Blue Rathgore", + "Blue Vein (Australian)", "Blue Vein Cheeses", "Bocconcini", "Bocconcini (Australian)", + "Boeren Leidenkaas", "Bonchester", "Bosworth", "Bougon", "Boule Du Roves", + "Boulette d'Avesnes", "Boursault", "Boursin", "Bouyssou", "Bra", "Braudostur", + "Breakfast Cheese", "Brebis du Lavort", "Brebis du Lochois", "Brebis du Puyfaucon", + "Bresse Bleu", "Brick", "Brie", "Brie de Meaux", "Brie de Melun", "Brillat-Savarin", + "Brin", "Brin d' Amour", "Brin d'Amour", "Brinza (Burduf Brinza)", + "Briquette de Brebis", "Briquette du Forez", "Broccio", "Broccio Demi-Affine", + "Brousse du Rove", "Bruder Basil", "Brusselae Kaas (Fromage de Bruxelles)", "Bryndza", + "Buchette d'Anjou", "Buffalo", "Burgos", "Butte", "Butterkase", "Button (Innes)", + "Buxton Blue", "Cabecou", "Caboc", "Cabrales", "Cachaille", "Caciocavallo", "Caciotta", + "Caerphilly", "Cairnsmore", "Calenzana", "Cambazola", "Camembert de Normandie", + "Canadian Cheddar", "Canestrato", "Cantal", "Caprice des Dieux", "Capricorn Goat", + "Capriole Banon", "Carre de l'Est", "Casciotta di Urbino", "Cashel Blue", "Castellano", + "Castelleno", "Castelmagno", "Castelo Branco", "Castigliano", "Cathelain", + "Celtic Promise", "Cendre d'Olivet", "Cerney", "Chabichou", "Chabichou du Poitou", + "Chabis de Gatine", "Chaource", "Charolais", "Chaumes", "Cheddar", + "Cheddar Clothbound", "Cheshire", "Chevres", "Chevrotin des Aravis", "Chontaleno", + "Civray", "Coeur de Camembert au Calvados", "Coeur de Chevre", "Colby", "Cold Pack", + "Comte", "Coolea", "Cooleney", "Coquetdale", "Corleggy", "Cornish Pepper", + "Cotherstone", "Cotija", "Cottage Cheese", "Cottage Cheese (Australian)", + "Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Cheese", + "Cream Havarti", "Crema Agria", "Crema Mexicana", "Creme Fraiche", "Crescenza", + "Croghan", "Crottin de Chavignol", "Crottin du Chavignol", "Crowdie", "Crowley", + "Cuajada", "Curd", "Cure Nantais", "Curworthy", "Cwmtawe Pecorino", + "Cypress Grove Chevre", "Danablu (Danish Blue)", "Danbo", "Danish Fontina", + "Daralagjazsky", "Dauphin", "Delice des Fiouves", "Denhany Dorset Drum", "Derby", + "Dessertnyj Belyj", "Devon Blue", "Devon Garland", "Dolcelatte", "Doolin", + "Doppelrhamstufel", "Dorset Blue Vinney", "Double Gloucester", "Double Worcester", + "Dreux a la Feuille", "Dry Jack", "Duddleswell", "Dunbarra", "Dunlop", "Dunsyre Blue", + "Duroblando", "Durrus", "Dutch Mimolette (Commissiekaas)", "Edam", "Edelpilz", + "Emental Grand Cru", "Emlett", "Emmental", "Epoisses de Bourgogne", "Esbareich", + "Esrom", "Etorki", "Evansdale Farmhouse Brie", "Evora De L'Alentejo", "Exmoor Blue", + "Explorateur", "Feta", "Feta (Australian)", "Figue", "Filetta", "Fin-de-Siecle", + "Finlandia Swiss", "Finn", "Fiore Sardo", "Fleur du Maquis", "Flor de Guia", + "Flower Marie", "Folded", "Folded cheese with mint", "Fondant de Brebis", + "Fontainebleau", "Fontal", "Fontina Val d'Aosta", "Formaggio di capra", "Fougerus", + "Four Herb Gouda", "Fourme d' Ambert", "Fourme de Haute Loire", "Fourme de Montbrison", + "Fresh Jack", "Fresh Mozzarella", "Fresh Ricotta", "Fresh Truffles", "Fribourgeois", + "Friesekaas", "Friesian", "Friesla", "Frinault", "Fromage a Raclette", "Fromage Corse", + "Fromage de Montagne de Savoie", "Fromage Frais", "Fruit Cream Cheese", + "Frying Cheese", "Fynbo", "Gabriel", "Galette du Paludier", "Galette Lyonnaise", + "Galloway Goat's Milk Gems", "Gammelost", "Gaperon a l'Ail", "Garrotxa", "Gastanberra", + "Geitost", "Gippsland Blue", "Gjetost", "Gloucester", "Golden Cross", "Gorgonzola", + "Gornyaltajski", "Gospel Green", "Gouda", "Goutu", "Gowrie", "Grabetto", "Graddost", + "Grafton Village Cheddar", "Grana", "Grana Padano", "Grand Vatel", + "Grataron d' Areches", "Gratte-Paille", "Graviera", "Greuilh", "Greve", + "Gris de Lille", "Gruyere", "Gubbeen", "Guerbigny", "Halloumi", + "Halloumy (Australian)", "Haloumi-Style Cheese", "Harbourne Blue", "Havarti", + "Heidi Gruyere", "Hereford Hop", "Herrgardsost", "Herriot Farmhouse", "Herve", + "Hipi Iti", "Hubbardston Blue Cow", "Hushallsost", "Iberico", "Idaho Goatster", + "Idiazabal", "Il Boschetto al Tartufo", "Ile d'Yeu", "Isle of Mull", "Jarlsberg", + "Jermi Tortes", "Jibneh Arabieh", "Jindi Brie", "Jubilee Blue", "Juustoleipa", + "Kadchgall", "Kaseri", "Kashta", "Kefalotyri", "Kenafa", "Kernhem", "Kervella Affine", + "Kikorangi", "King Island Cape Wickham Brie", "King River Gold", "Klosterkaese", + "Knockalara", "Kugelkase", "L'Aveyronnais", "L'Ecir de l'Aubrac", "La Taupiniere", + "La Vache Qui Rit", "Laguiole", "Lairobell", "Lajta", "Lanark Blue", "Lancashire", + "Langres", "Lappi", "Laruns", "Lavistown", "Le Brin", "Le Fium Orbo", "Le Lacandou", + "Le Roule", "Leafield", "Lebbene", "Leerdammer", "Leicester", "Leyden", "Limburger", + "Lincolnshire Poacher", "Lingot Saint Bousquet d'Orb", "Liptauer", "Little Rydings", + "Livarot", "Llanboidy", "Llanglofan Farmhouse", "Loch Arthur Farmhouse", + "Loddiswell Avondale", "Longhorn", "Lou Palou", "Lou Pevre", "Lyonnais", "Maasdam", + "Macconais", "Mahoe Aged Gouda", "Mahon", "Malvern", "Mamirolle", "Manchego", + "Manouri", "Manur", "Marble Cheddar", "Marbled Cheeses", "Maredsous", "Margotin", + "Maribo", "Maroilles", "Mascares", "Mascarpone", "Mascarpone (Australian)", + "Mascarpone Torta", "Matocq", "Maytag Blue", "Meira", "Menallack Farmhouse", + "Menonita", "Meredith Blue", "Mesost", "Metton (Cancoillotte)", "Meyer Vintage Gouda", + "Mihalic Peynir", "Milleens", "Mimolette", "Mine-Gabhar", "Mini Baby Bells", "Mixte", + "Molbo", "Monastery Cheeses", "Mondseer", "Mont D'or Lyonnais", "Montasio", + "Monterey Jack", "Monterey Jack Dry", "Morbier", "Morbier Cru de Montagne", + "Mothais a la Feuille", "Mozzarella", "Mozzarella (Australian)", + "Mozzarella di Bufala", "Mozzarella Fresh, in water", "Mozzarella Rolls", "Munster", + "Murol", "Mycella", "Myzithra", "Naboulsi", "Nantais", "Neufchatel", + "Neufchatel (Australian)", "Niolo", "Nokkelost", "Northumberland", "Oaxaca", + "Olde York", "Olivet au Foin", "Olivet Bleu", "Olivet Cendre", + "Orkney Extra Mature Cheddar", "Orla", "Oschtjepka", "Ossau Fermier", "Ossau-Iraty", + "Oszczypek", "Oxford Blue", "P'tit Berrichon", "Palet de Babligny", "Paneer", "Panela", + "Pannerone", "Pant ys Gawn", "Parmesan (Parmigiano)", "Parmigiano Reggiano", + "Pas de l'Escalette", "Passendale", "Pasteurized Processed", "Pate de Fromage", + "Patefine Fort", "Pave d'Affinois", "Pave d'Auge", "Pave de Chirac", "Pave du Berry", + "Pecorino", "Pecorino in Walnut Leaves", "Pecorino Romano", "Peekskill Pyramid", + "Pelardon des Cevennes", "Pelardon des Corbieres", "Penamellera", "Penbryn", + "Pencarreg", "Perail de Brebis", "Petit Morin", "Petit Pardou", "Petit-Suisse", + "Picodon de Chevre", "Picos de Europa", "Piora", "Pithtviers au Foin", + "Plateau de Herve", "Plymouth Cheese", "Podhalanski", "Poivre d'Ane", "Polkolbin", + "Pont l'Eveque", "Port Nicholson", "Port-Salut", "Postel", "Pouligny-Saint-Pierre", + "Pourly", "Prastost", "Pressato", "Prince-Jean", "Processed Cheddar", "Provolone", + "Provolone (Australian)", "Pyengana Cheddar", "Pyramide", "Quark", + "Quark (Australian)", "Quartirolo Lombardo", "Quatre-Vents", "Quercy Petit", + "Queso Blanco", "Queso Blanco con Frutas --Pina y Mango", "Queso de Murcia", + "Queso del Montsec", "Queso del Tietar", "Queso Fresco", "Queso Fresco (Adobera)", + "Queso Iberico", "Queso Jalapeno", "Queso Majorero", "Queso Media Luna", + "Queso Para Frier", "Queso Quesadilla", "Rabacal", "Raclette", "Ragusano", "Raschera", + "Reblochon", "Red Leicester", "Regal de la Dombes", "Reggianito", "Remedou", + "Requeson", "Richelieu", "Ricotta", "Ricotta (Australian)", "Ricotta Salata", "Ridder", + "Rigotte", "Rocamadour", "Rollot", "Romano", "Romans Part Dieu", "Roncal", "Roquefort", + "Roule", "Rouleau De Beaulieu", "Royalp Tilsit", "Rubens", "Rustinu", "Saaland Pfarr", + "Saanenkaese", "Saga", "Sage Derby", "Sainte Maure", "Saint-Marcellin", + "Saint-Nectaire", "Saint-Paulin", "Salers", "Samso", "San Simon", "Sancerre", + "Sap Sago", "Sardo", "Sardo Egyptian", "Sbrinz", "Scamorza", "Schabzieger", "Schloss", + "Selles sur Cher", "Selva", "Serat", "Seriously Strong Cheddar", "Serra da Estrela", + "Sharpam", "Shelburne Cheddar", "Shropshire Blue", "Siraz", "Sirene", "Smoked Gouda", + "Somerset Brie", "Sonoma Jack", "Sottocenare al Tartufo", "Soumaintrain", + "Sourire Lozerien", "Spenwood", "Sraffordshire Organic", "St. Agur Blue Cheese", + "Stilton", "Stinking Bishop", "String", "Sussex Slipcote", "Sveciaost", "Swaledale", + "Sweet Style Swiss", "Swiss", "Syrian (Armenian String)", "Tala", "Taleggio", "Tamie", + "Tasmania Highland Chevre Log", "Taupiniere", "Teifi", "Telemea", "Testouri", + "Tete de Moine", "Tetilla", "Texas Goat Cheese", "Tibet", "Tillamook Cheddar", + "Tilsit", "Timboon Brie", "Toma", "Tomme Brulee", "Tomme d'Abondance", + "Tomme de Chevre", "Tomme de Romans", "Tomme de Savoie", "Tomme des Chouans", "Tommes", + "Torta del Casar", "Toscanello", "Touree de L'Aubier", "Tourmalet", + "Trappe (Veritable)", "Trois Cornes De Vendee", "Tronchon", "Trou du Cru", "Truffe", + "Tupi", "Turunmaa", "Tymsboro", "Tyn Grug", "Tyning", "Ubriaco", "Ulloa", + "Vacherin-Fribourgeois", "Valencay", "Vasterbottenost", "Venaco", "Vendomois", + "Vieux Corse", "Vignotte", "Vulscombe", "Waimata Farmhouse Blue", + "Washed Rind Cheese (Australian)", "Waterloo", "Weichkaese", "Wellington", + "Wensleydale", "White Stilton", "Whitestone Farmhouse", "Wigmore", "Woodside Cabecou", + "Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue", + "Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano" +) + +data class Cheese( + val id: Long, + val name: String, + @DrawableRes val image: Int +) { + companion object { + fun all(): List { + return CheeseNames.mapIndexed { i, name -> + Cheese( + id = (i + 1).toLong(), + name = name, + image = CheeseImages[ + ((name.hashCode() % CheeseImages.size) + CheeseImages.size) % + CheeseImages.size + ] + ) + } + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/SimpleScaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/SimpleScaffold.kt new file mode 100644 index 000000000..987d545a3 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/SimpleScaffold.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SimpleScaffold( + title: String, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = title) + }, + modifier = Modifier + .statusBarsPadding() + ) + }, + modifier = modifier + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + content() + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/custom/CustomCanvasAnimation.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/custom/CustomCanvasAnimation.kt new file mode 100644 index 000000000..e82794346 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/custom/CustomCanvasAnimation.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.custom + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.spring +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +/** + * A custom loading animation example using Canvas and draw APIs, combined with + * Animatable to show the use of the animateTo() function used sequentially. + */ +@Composable +@Preview +fun CustomCanvasBouncyLoader() { + val yOffset = remember { + Animatable(0f) + } + val scale = remember { + Animatable(1f) + } + LaunchedEffect("bouncyLoader") { + delay(400) + // We use the Animatable.animateTo() API here to demonstrate the coroutine usage - each + // item is animating one after the other, as the animateTo function is sequential. + // Animate y to half the height + yOffset.animateTo(0.5f, bouncyAnimationSpec) + scale.animateTo(3f, bouncyAnimationSpec) + delay(500) + scale.animateTo(10f, bouncyAnimationSpec) + delay(500) + scale.animateTo(50f, bouncyAnimationSpec) + } + val size = remember { + mutableStateOf(IntSize.Zero) + } + Box( + Modifier + .fillMaxSize() + .onSizeChanged { + // We get the size change of the whole composable, and use this to determine how + // big the ball should be. + size.value = it + } + ) { + GradientCircle( + Modifier + .align(Alignment.TopCenter) + .size(26.dp) + .graphicsLayer { + // We use .graphicsLayer here to perform the animation as we are animating + // multiple properties of our Gradient circle at once, and this is more + // efficient than using multiple modifiers. + // .graphicsLayer also defers these changes to the Draw phase of Compose, + // therefore minimizing recompositions required to do this. + scaleX = scale.value + scaleY = scale.value + translationY = yOffset.value * size.value.height + } + ) + } +} + +@Composable +private fun GradientCircle(modifier: Modifier = Modifier) { + val brush = remember { + Brush.verticalGradient(listOf(Color(0xFFF56E34), Color(0xFF234EDA))) + } + Canvas(modifier = modifier) { + drawCircle(brush = brush) + } +} + +private val bouncyAnimationSpec: SpringSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow +) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fade/FadeDemo.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fade/FadeDemo.kt new file mode 100644 index 000000000..c46735f25 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fade/FadeDemo.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.fade + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.animations.demo.CheeseImages +import com.example.compose.snippets.animations.demo.SimpleScaffold + +/** + * A fade creates a smooth sequence between elements that fully overlap each other, such as + * photos inside of a card or another container. When a new element enters, it fades in + * over the current element. + */ +@Preview +@Composable +fun FadeDemo() { + SimpleScaffold(title = "Fade") { + + val painters = CheeseImages.map { painterResource(it) } + var index by remember { mutableIntStateOf(0) } + + AnimatedContent( + targetState = index, + modifier = Modifier.align(Alignment.Center), + transitionSpec = fade() + ) { targetIndex -> + Image( + painter = painters[targetIndex], + contentDescription = "Cheese", + modifier = Modifier + .size(256.dp, 192.dp) + .clip(shape = RoundedCornerShape(16.dp)), + contentScale = ContentScale.Crop + ) + } + + Button( + onClick = { index = (index + 1) % painters.size }, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(64.dp) + ) { + Text(text = "NEXT") + } + } +} + +/** + * Creates a transitionSpec for configuring [AnimatedContent] to the fade pattern. + */ +private fun fade( + durationMillis: Int = 300 +): AnimatedContentTransitionScope.() -> ContentTransform { + return { + ContentTransform( + // The initial content should stay until the target content is completely opaque. + initialContentExit = fadeOut(animationSpec = snap(delayMillis = durationMillis)), + // The target content fades in. This is shown on top of the initial content. + targetContentEnter = fadeIn( + animationSpec = tween( + durationMillis = durationMillis, + // LinearOutSlowInEasing is suitable for incoming elements. + easing = LinearOutSlowInEasing + ) + ) + ) + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fadethrough/FadeThroughDemo.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fadethrough/FadeThroughDemo.kt new file mode 100644 index 000000000..efdd35745 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fadethrough/FadeThroughDemo.kt @@ -0,0 +1,256 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.fadethrough + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R +import com.example.compose.snippets.animations.demo.SimpleScaffold + +/** + * Fade through involves one element fading out completely before a new one fades in. These + * transitions can be applied to text, icons, and other elements that don't perfectly + * overlap. This technique lets the background show through during a transition, and it can + * provide continuity between screens when paired with a shared transformation. + */ +@Preview +@Composable +fun FadeThroughDemo() { + SimpleScaffold(title = "Fade through") { + var expanded by rememberSaveable { mutableStateOf(false) } + DemoCard( + expanded = expanded, + modifier = Modifier.align(Alignment.Center) + ) + + Button( + onClick = { expanded = !expanded }, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(64.dp) + ) { + Text(text = "TOGGLE") + } + } +} + +/** + * Shows the card. The card can be either expanded or collapsed, and the transition between the two + * states is animated with the fade-through effect. + * + * @param expanded Whether the card is expanded or collapsed. + */ +@Composable +private fun DemoCard( + expanded: Boolean, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier, + tonalElevation = 2.dp, + shape = RoundedCornerShape(16.dp) + ) { + // Use `AnimatedContent` to switch between different content. + AnimatedContent( + // `targetState` specifies the input state. + targetState = expanded, + // `transitionSpec` defines the behavior of the transition animation. + transitionSpec = fadeThrough() + ) { targetExpanded -> + if (targetExpanded) { + ExpandedContent() + } else { + CollapsedContent() + } + } + } +} + +/** + * Creates a transitionSpec for configuring [AnimatedContent] to the fade through pattern. + * See [Fade through](https://material.io/design/motion/the-motion-system.html#fade-through) for + * the motion spec. + */ +fun fadeThrough( + durationMillis: Int = 300 +): AnimatedContentTransitionScope.() -> ContentTransform { + return { + ContentTransform( + // The initial content fades out. + initialContentExit = fadeOut( + animationSpec = tween( + // The duration is 3/8 of the overall duration. + durationMillis = durationMillis * 3 / 8, + // FastOutLinearInEasing is suitable for elements exiting the screen. + easing = FastOutLinearInEasing + ) + ), + // The target content fades in after the current content fades out. + targetContentEnter = fadeIn( + animationSpec = tween( + // The duration is 5/8 of the overall duration. + durationMillis = durationMillis * 5 / 8, + // Delays the EnterTransition by the duration of the ExitTransition. + delayMillis = durationMillis * 3 / 8, + // LinearOutSlowInEasing is suitable for incoming elements. + easing = LinearOutSlowInEasing + ) + ), + // The size changes along with the content transition. + sizeTransform = SizeTransform( + sizeAnimationSpec = { _, _ -> + tween(durationMillis = durationMillis) + } + ) + ) + } +} + +@Composable +private fun CollapsedContent() { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Image( + painter = painterResource(R.drawable.cheese_1), + contentDescription = null, + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + ) + Text( + text = "Cheese", + style = MaterialTheme.typography.headlineSmall + ) + } +} + +@Composable +private fun ExpandedContent() { + Column( + modifier = Modifier.width(IntrinsicSize.Min) + ) { + Image( + painter = painterResource(R.drawable.cheese_1), + contentDescription = null, + modifier = Modifier.size(320.dp, 128.dp), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Cheese", + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Text( + text = "Hello, world", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.lorem_ipsum), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 16.dp), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + TextButton(onClick = { /* Do nothing */ }) { + Text(text = "DETAIL") + } + TextButton(onClick = { /* Do nothing */ }) { + Text(text = "ORDER") + } + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { /* Do nothing */ }) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = "Favorite" + ) + } + IconButton(onClick = { /* Do nothing */ }) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = "Share" + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Preview +@Composable +private fun PreviewDemoCardCollapsed() { + DemoCard(expanded = false) +} + +@Preview +@Composable +private fun PreviewDemoCardExpanded() { + DemoCard(expanded = true) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedaxis/SharedAxisDemo.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedaxis/SharedAxisDemo.kt new file mode 100644 index 000000000..ceb7b4a61 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedaxis/SharedAxisDemo.kt @@ -0,0 +1,282 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.sharedaxis + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.animations.demo.CheeseImages +import com.example.compose.snippets.animations.demo.CheeseNames +import com.example.compose.snippets.animations.demo.SimpleScaffold + +/** + * The shared axis pattern is used for transitions between UI elements that have a spatial + * or navigational relationship. This demo uses a shared transformation on the Y-axis to + * reinforce the sequential order of elements. + */ +@Composable +fun SharedAxisDemo() { + SimpleScaffold(title = "Layout > Shared axis (Y-axis)") { + val pages = remember { createPages() } + // Indicator column + var id by rememberSaveable { mutableIntStateOf(1) } + Row(modifier = Modifier.padding(end = 16.dp)) { + PageIndicatorsColumn( + pages = pages, + selectedId = id, + onIndicatorClick = { id = it } + ) + + // SharedYAxis animates the content change. + SharedYAxis(targetState = pages.first { it.id == id }) { page -> + PageContent(page = page) + } + } + } +} + +/** + * Animates content change with the vertical shared axis pattern. + * + * See [Shared axis](https://material.io/design/motion/the-motion-system.html#shared-axis) for the + * detail about this motion pattern. + */ +@Composable +private fun > SharedYAxis( + targetState: T, + modifier: Modifier = Modifier, + content: @Composable AnimatedVisibilityScope.(T) -> Unit +) { + val exitDurationMillis = 80 + val enterDurationMillis = 220 + + // This local function creates the AnimationSpec for outgoing elements. + fun exitSpec(): FiniteAnimationSpec = + tween( + durationMillis = exitDurationMillis, + easing = FastOutLinearInEasing + ) + + // This local function creates the AnimationSpec for incoming elements. + fun enterSpec(): FiniteAnimationSpec = + tween( + // The enter animation runs right after the exit animation. + delayMillis = exitDurationMillis, + durationMillis = enterDurationMillis, + easing = LinearOutSlowInEasing + ) + + val slideDistance = with(LocalDensity.current) { 30.dp.roundToPx() } + + AnimatedContent( + targetState = targetState, + transitionSpec = { + // The state type () is Comparable. + // We compare the initial state and the target state to determine whether we are moving + // down or up. + if (initialState < targetState) { // Move down + ContentTransform( + // Outgoing elements fade out and slide up to the top. + initialContentExit = fadeOut(exitSpec()) + + slideOutVertically(exitSpec()) { -slideDistance }, + // Incoming elements fade in and slide up from the bottom. + targetContentEnter = fadeIn(enterSpec()) + + slideInVertically(enterSpec()) { slideDistance } + ) + } else { // Move up + ContentTransform( + // Outgoing elements fade out and slide down to the bottom. + initialContentExit = fadeOut(exitSpec()) + + slideOutVertically(exitSpec()) { slideDistance }, + // Outgoing elements fade in and slide down from the top. + targetContentEnter = fadeIn(enterSpec()) + + slideInVertically(enterSpec()) { -slideDistance } + ) + } + }, + modifier = modifier, + content = content + ) +} + +private class Page( + val id: Int, + @DrawableRes + val image: Int, + val title: String, + val body: String +) : Comparable { + + override fun compareTo(other: Page): Int { + return id.compareTo(other.id) + } +} + +private fun createPages(): List { + val body = LoremIpsum().values.joinToString(separator = " ").replace('\n', ' ') + return (0..4).map { i -> + Page( + id = i + 1, + image = CheeseImages[i % CheeseImages.size], + title = CheeseNames[i * 128], + body = body + ) + } +} + +@Composable +private fun PageIndicatorsColumn( + pages: List, + selectedId: Int, + onIndicatorClick: (index: Int) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + for (page in pages) { + PageIndicator( + index = page.id, + selected = selectedId == page.id, + onClick = { onIndicatorClick(page.id) } + ) + } + } +} + +@Composable +private fun PageIndicator( + index: Int, + selected: Boolean, + onClick: () -> Unit +) { + val transition = updateTransition(targetState = selected, label = "indicator") + val backgroundColor by transition.animateColor(label = "background color") { targetSelected -> + if (targetSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceVariant + } + } + val textColor by transition.animateColor(label = "text color") { targetSelected -> + if (targetSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + } + IconButton(onClick = onClick) { + Text( + text = index.toString(), + modifier = Modifier + .size(32.dp) + .background(backgroundColor, CircleShape) + .wrapContentSize(), + color = textColor, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +private fun PageContent( + page: Page, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .navigationBarsPadding(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Image( + painter = painterResource(page.image), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(16f / 9f) + .clip(RoundedCornerShape(16.dp)), + contentScale = ContentScale.Crop + ) + Text( + text = page.title, + style = MaterialTheme.typography.titleLarge + ) + Text( + text = page.body, + textAlign = TextAlign.Justify + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewSharedAxisDemo() { + SharedAxisDemo() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedtransform/SharedTransformDemo.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedtransform/SharedTransformDemo.kt new file mode 100644 index 000000000..3e913c782 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedtransform/SharedTransformDemo.kt @@ -0,0 +1,261 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.sharedtransform + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import com.example.compose.snippets.R +import com.example.compose.snippets.animations.demo.SimpleScaffold +import com.example.compose.snippets.animations.demo.fadethrough.fadeThrough +import com.example.compose.snippets.ui.theme.SnippetsTheme + +/** + * Complex layout changes use a shared transformation to create smooth transitions from + * one layout to the next. Elements are grouped together and transform as a single unit, + * rather than animating independently. This avoids multiple transformations overlapping + * and competing for attention. + */ +@Composable +fun SharedTransformDemo() { + SimpleScaffold(title = "Shared transform") { + DemoCard( + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 32.dp) + .widthIn(max = 384.dp) + .fillMaxWidth() + ) + } +} + +@Composable +private fun DemoCard( + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier, + tonalElevation = 2.dp, + shape = RoundedCornerShape(16.dp), + ) { + // The content of this card is laid out by this ConstraintLayout. + ConstraintLayout { + // The card is either expanded or collapsed. + var expanded by rememberSaveable { mutableStateOf(false) } + + // The ConstraintLayout has 4 constrained elements. They animate separately during the + // animation, except for the icon that is shared in both the expanded and the + // collapsed states. + val (content, icon, divider, button) = createRefs() + + // This transition object coordinates different kinds of animations. + val transition = updateTransition(targetState = expanded, label = "card") + + // This is the main content of the card. + // By using the AnimatedContent composable as an extension function of the transition + // object, the animation runs in sync with other animations of the transition. + // The height of this element animates on the state change (SizeTransform), and the + // ConstraintLayout can lay out its children based on the constraints continuously + // during the animation. + transition.AnimatedContent( + // We use the fade-through effect for elements that change between the states. + transitionSpec = fadeThrough(), + modifier = Modifier.constrainAs(content) { + top.linkTo(parent.top, margin = 16.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { targetExpanded -> + CardContent(expanded = targetExpanded) + } + + // The icon is shared between the expanded and collapsed states. + CardIcon( + modifier = Modifier.constrainAs(icon) { + top.linkTo(parent.top, margin = 16.dp) + end.linkTo(parent.end, margin = 16.dp) + } + ) + + // The divider becomes transparent in the collapsed state. + val dividerColor by transition.animateColor(label = "divider color") { targetExpanded -> + if (targetExpanded) { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) + } else { + Color.Transparent + } + } + HorizontalDivider( + modifier = Modifier.constrainAs(divider) { + top.linkTo(content.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + color = dividerColor + ) + + // The expand/collapse button is shared between the expanded and collapsed states. + TextButton( + onClick = { expanded = !expanded }, + modifier = Modifier.constrainAs(button) { + top.linkTo(divider.bottom, margin = 8.dp) + start.linkTo(parent.start, margin = 8.dp) + // The button is constrained to the bottom of the parent so that it remains + // visible during the animations. + bottom.linkTo(parent.bottom, margin = 8.dp) + } + ) { + // The AnimatedContent extension function can be used for any descendant elements, + // not just direct children. + transition.AnimatedContent(transitionSpec = fadeThrough()) { targetExpanded -> + Text(text = if (targetExpanded) "COLLAPSE" else "EXPAND") + } + } + } + } +} + +private val CheeseImages = listOf( + R.drawable.cheese_1 to "Cheese 1", + R.drawable.cheese_2 to "Cheese 2", + R.drawable.cheese_3 to "Cheese 3", + R.drawable.cheese_4 to "Cheese 4", + R.drawable.cheese_5 to "Cheese 5" +) + +@Composable +private fun CardContent(expanded: Boolean, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + if (expanded) { + ContentTitle(modifier = Modifier.padding(horizontal = 16.dp)) + ContentMaker( + modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) + ) + ContentImagesRow(images = CheeseImages.subList(0, 2)) + Spacer(modifier = Modifier.height(1.dp)) + ContentImagesRow(images = CheeseImages.subList(2, 5)) + ContentBody(maxLines = 2, modifier = Modifier.padding(16.dp)) + } else { + ContentMaker(modifier = Modifier.padding(horizontal = 16.dp)) + ContentTitle(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) + ContentBody(maxLines = 1, Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)) + ContentImagesRow(images = CheeseImages) + } + } +} + +@Composable +private fun ContentTitle(modifier: Modifier = Modifier) { + Text( + text = "Cheeses", + modifier = modifier, + style = MaterialTheme.typography.titleLarge + ) +} + +@Composable +private fun ContentMaker(modifier: Modifier = Modifier) { + Text( + text = "Maker: Android Cheese", + modifier = modifier, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + ) +} + +@Composable +private fun CardIcon(modifier: Modifier = Modifier) { + Image( + painter = painterResource(R.drawable.cheese_1), + contentDescription = null, + modifier = modifier + .size(48.dp) + .clip(CircleShape) + ) +} + +@Composable +private fun ContentImagesRow(images: List>, modifier: Modifier = Modifier) { + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(1.dp)) { + for ((resourceId, contentDescription) in images) { + Image( + painter = painterResource(resourceId), + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = Modifier + .weight(1f) + .aspectRatio(1f) + ) + } + } +} + +@Composable +private fun ContentBody(maxLines: Int, modifier: Modifier = Modifier) { + Text( + text = LoremIpsum(32).values.joinToString(" ").replace('\n', ' '), + modifier = modifier, + style = MaterialTheme.typography.bodyMedium, + maxLines = maxLines, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Justify, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + ) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewExpandedContent() { + SnippetsTheme { + SharedTransformDemo() + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt new file mode 100644 index 000000000..e07cbec6f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementBlurSnippet.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.BlurEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +private val listSnacks = listOf( + Snack("Cupcake", "", R.drawable.cupcake), + Snack("Donut", "", R.drawable.donut), + Snack("Eclair", "", R.drawable.eclair), + Snack("Froyo", "", R.drawable.froyo), + Snack("Gingerbread", "", R.drawable.gingerbread), + Snack("Honeycomb", "", R.drawable.honeycomb), +) + +private fun animationSpec() = tween(durationMillis = 500) +private val boundsTransition = BoundsTransform { _, _ -> animationSpec() } +private val shapeForSharedElement = RoundedCornerShape(16.dp) + +@OptIn(ExperimentalSharedTransitionApi::class) +@Preview +@Composable +private fun AnimatedVisibilitySharedElementBlurLayer() { + var selectedSnack by remember { mutableStateOf(null) } + val graphicsLayer = rememberGraphicsLayer() + val animateBlurRadius = animateFloatAsState( + targetValue = if (selectedSnack != null) 20f else 0f, + label = "blur radius", + animationSpec = animationSpec() + ) + + SharedTransitionLayout(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(Color.LightGray.copy(alpha = 0.5f)) + .blurLayer(graphicsLayer, animateBlurRadius.value) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + itemsIndexed(listSnacks, key = { index, snack -> snack.name }) { index, snack -> + SnackItem( + snack = snack, + onClick = { + selectedSnack = snack + }, + visible = selectedSnack != snack, + modifier = Modifier.animateItem( + placementSpec = animationSpec(), + fadeOutSpec = animationSpec(), + fadeInSpec = animationSpec() + ) + ) + } + } + + SnackEditDetails( + snack = selectedSnack, + onConfirmClick = { + selectedSnack = null + } + ) + } +} + +fun Modifier.blurLayer(layer: GraphicsLayer, radius: Float): Modifier { + return if (radius == 0f) this else this.drawWithContent { + layer.apply { + record { + this@drawWithContent.drawContent() + } + // will apply a blur on API 31+ + this.renderEffect = BlurEffect(radius, radius, TileMode.Decal) + } + drawLayer(layer) + } +} +@Composable +fun SharedTransitionScope.SnackItem( + snack: Snack, + visible: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = fadeIn(animationSpec = animationSpec()) + scaleIn( + animationSpec() + ), + exit = fadeOut(animationSpec = animationSpec()) + scaleOut( + animationSpec() + ) + ) { + Box( + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "${snack.name}-bounds"), + animatedVisibilityScope = this, + boundsTransform = boundsTransition, + clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement) + ) + .background(Color.White, shapeForSharedElement) + .clip(shapeForSharedElement) + ) { + SnackContents( + snack = snack, + modifier = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = snack.name), + animatedVisibilityScope = this@AnimatedVisibility, + boundsTransform = boundsTransition, + ), + onClick = onClick + ) + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt new file mode 100644 index 000000000..5a51b8d56 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/AnimatedVisibilitySharedElementSnippets.kt @@ -0,0 +1,232 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +private val listSnacks = listOf( + Snack("Cupcake", "", R.drawable.cupcake), + Snack("Donut", "", R.drawable.donut), + Snack("Eclair", "", R.drawable.eclair), + Snack("Froyo", "", R.drawable.froyo), + Snack("Gingerbread", "", R.drawable.gingerbread), + Snack("Honeycomb", "", R.drawable.honeycomb), +) + +private val shapeForSharedElement = RoundedCornerShape(16.dp) + +@OptIn(ExperimentalSharedTransitionApi::class) +@Preview +@Composable +private fun AnimatedVisibilitySharedElementShortenedExample() { + // [START android_compose_shared_elements_animated_visibility] + var selectedSnack by remember { mutableStateOf(null) } + + SharedTransitionLayout(modifier = Modifier.fillMaxSize()) { + LazyColumn( + // [START_EXCLUDE] + modifier = Modifier + .fillMaxSize() + .background(Color.LightGray.copy(alpha = 0.5f)) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + // [END_EXCLUDE] + ) { + items(listSnacks) { snack -> + AnimatedVisibility( + visible = snack != selectedSnack, + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + modifier = Modifier.animateItem() + ) { + Box( + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "${snack.name}-bounds"), + // Using the scope provided by AnimatedVisibility + animatedVisibilityScope = this, + clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement) + ) + .background(Color.White, shapeForSharedElement) + .clip(shapeForSharedElement) + ) { + SnackContents( + snack = snack, + modifier = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = snack.name), + animatedVisibilityScope = this@AnimatedVisibility + ), + onClick = { + selectedSnack = snack + } + ) + } + } + } + } + // Contains matching AnimatedContent with sharedBounds modifiers. + SnackEditDetails( + snack = selectedSnack, + onConfirmClick = { + selectedSnack = null + } + ) + } + // [END android_compose_shared_elements_animated_visibility] +} + +@Composable +fun SharedTransitionScope.SnackEditDetails( + snack: Snack?, + modifier: Modifier = Modifier, + onConfirmClick: () -> Unit +) { + AnimatedContent( + modifier = modifier, + targetState = snack, + transitionSpec = { + fadeIn() togetherWith fadeOut() + }, + label = "SnackEditDetails" + ) { targetSnack -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (targetSnack != null) { + Box( + modifier = Modifier + .fillMaxSize() + .clickable { + onConfirmClick() + } + .background(Color.Black.copy(alpha = 0.5f)) + ) + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "${targetSnack.name}-bounds"), + animatedVisibilityScope = this@AnimatedContent, + clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement) + ) + .background(Color.White, shapeForSharedElement) + .clip(shapeForSharedElement) + ) { + + SnackContents( + snack = targetSnack, + modifier = Modifier.sharedElement( + sharedContentState = rememberSharedContentState(key = targetSnack.name), + animatedVisibilityScope = this@AnimatedContent, + ), + onClick = { + onConfirmClick() + } + ) + Row( + Modifier + .fillMaxWidth() + .padding(bottom = 8.dp, end = 8.dp), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = { onConfirmClick() }) { + Text(text = "Save changes") + } + } + } + } + } + } +} + +@Composable +fun SnackContents( + snack: Snack, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Column( + modifier = modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + onClick() + } + ) { + Image( + painter = painterResource(id = snack.image), + modifier = Modifier + .fillMaxWidth() + .aspectRatio(20f / 9f), + contentScale = ContentScale.Crop, + contentDescription = null + ) + Text( + text = snack.name, + modifier = Modifier + .wrapContentWidth() + .padding(8.dp), + style = MaterialTheme.typography.titleSmall + ) + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/BasicSharedElementSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/BasicSharedElementSnippets.kt new file mode 100644 index 000000000..c246a8098 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/BasicSharedElementSnippets.kt @@ -0,0 +1,499 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.R +import com.example.compose.snippets.ui.theme.LavenderLight +import com.example.compose.snippets.ui.theme.RoseLight + +private class SharedElementBasicUsage2 { + @Preview + @Composable + private fun SharedElementApp() { + // [START android_compose_animations_shared_element_step1] + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + MainContent( + onShowDetails = { + showDetails = true + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } else { + DetailsContent( + onBack = { + showDetails = false + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } + } + } + // [END android_compose_animations_shared_element_step1] + } + + @Composable + private fun MainContent( + onShowDetails: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope + ) { + Row( + // [START_EXCLUDE] + modifier = Modifier + .padding(8.dp) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(LavenderLight, RoundedCornerShape(8.dp)) + .clickable { + onShowDetails() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .size(100.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START_EXCLUDE] + Text("Cupcake", fontSize = 21.sp) + // [END_EXCLUDE] + } + } + + @Composable + private fun DetailsContent( + modifier: Modifier = Modifier, + onBack: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope + ) { + Column( + // [START_EXCLUDE] + modifier = Modifier + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(RoseLight, RoundedCornerShape(8.dp)) + .clickable { + onBack() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .size(200.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START_EXCLUDE] + Text("Cupcake", fontSize = 28.sp) + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + ) + // [END_EXCLUDE] + } + } +} + +private class SharedElementBasicUsage3 { + + @Preview + @Composable + private fun SharedElementApp() { + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + MainContent( + onShowDetails = { + showDetails = true + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } else { + DetailsContent( + onBack = { + showDetails = false + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } + } + } + } + + // [START android_compose_animations_shared_element_step2] + @Composable + private fun MainContent( + onShowDetails: () -> Unit, + modifier: Modifier = Modifier, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope + ) { + Row( + // [START_EXCLUDE] + modifier = Modifier + .padding(8.dp) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(LavenderLight, RoundedCornerShape(8.dp)) + .clickable { + onShowDetails() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + with(sharedTransitionScope) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(100.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START_EXCLUDE] + Text( + "Cupcake", fontSize = 21.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + // [END_EXCLUDE] + } + } + } + + @Composable + private fun DetailsContent( + modifier: Modifier = Modifier, + onBack: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope + ) { + Column( + // [START_EXCLUDE] + modifier = Modifier + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(RoseLight, RoundedCornerShape(8.dp)) + .clickable { + onBack() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + with(sharedTransitionScope) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(200.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START_EXCLUDE] + Text( + "Cupcake", fontSize = 28.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + ) + // [END_EXCLUDE] + } + } + } + // [END android_compose_animations_shared_element_step2] +} + +@Preview +@Composable +private fun SharedElement_ManualVisibleControl() { + // [START android_compose_shared_element_manual_control] + var selectFirst by remember { mutableStateOf(true) } + val key = remember { Any() } + SharedTransitionLayout( + Modifier + .fillMaxSize() + .padding(10.dp) + .clickable { + selectFirst = !selectFirst + } + ) { + Box( + Modifier + .sharedElementWithCallerManagedVisibility( + rememberSharedContentState(key = key), + !selectFirst + ) + .background(Color.Red) + .size(100.dp) + ) { + Text(if (!selectFirst) "false" else "true", color = Color.White) + } + Box( + Modifier + .offset(180.dp, 180.dp) + .sharedElementWithCallerManagedVisibility( + rememberSharedContentState( + key = key, + ), + selectFirst + ) + .alpha(0.5f) + .background(Color.Blue) + .size(180.dp) + ) { + Text(if (selectFirst) "false" else "true", color = Color.White) + } + } + // [END android_compose_shared_element_manual_control] +} + +@Preview +@Composable +private fun UnmatchedBoundsExample() { + // [START android_compose_animation_shared_element_bounds_unmatched] + var selectFirst by remember { mutableStateOf(true) } + val key = remember { Any() } + SharedTransitionLayout( + Modifier + .fillMaxSize() + .padding(10.dp) + .clickable { + selectFirst = !selectFirst + } + ) { + AnimatedContent(targetState = selectFirst, label = "AnimatedContent") { targetState -> + if (targetState) { + Box( + Modifier + .padding(12.dp) + .sharedBounds( + rememberSharedContentState(key = key), + animatedVisibilityScope = this@AnimatedContent + ) + .border(2.dp, Color.Red) + ) { + Text( + "Hello", + fontSize = 20.sp + ) + } + } else { + Box( + Modifier + .offset(180.dp, 180.dp) + .sharedBounds( + rememberSharedContentState( + key = key, + ), + animatedVisibilityScope = this@AnimatedContent + ) + .border(2.dp, Color.Red) + // This padding is placed after sharedBounds, but it doesn't match the + // other shared elements modifier order, resulting in visual jumps + .padding(12.dp) + + ) { + Text( + "Hello", + fontSize = 36.sp + ) + } + } + } + } + // [END android_compose_animation_shared_element_bounds_unmatched] +} + +private object UniqueKeySnippet { + // [START android_compose_shared_elements_unique_key] + data class SnackSharedElementKey( + val snackId: Long, + val origin: String, + val type: SnackSharedElementType + ) + + enum class SnackSharedElementType { + Bounds, + Image, + Title, + Tagline, + Background + } + + @Composable + fun SharedElementUniqueKey() { + // [START_EXCLUDE] + SharedTransitionLayout { + AnimatedVisibility(visible = true) { + // [END_EXCLUDE] + Box( + modifier = Modifier + .sharedElement( + rememberSharedContentState( + key = SnackSharedElementKey( + snackId = 1, + origin = "latest", + type = SnackSharedElementType.Image + ) + ), + animatedVisibilityScope = this@AnimatedVisibility + ) + ) + // [START_EXCLUDE] + } + } + // [END_EXCLUDE] + } + // [END android_compose_shared_elements_unique_key] +} + +// [START android_compose_shared_element_scope] +val LocalNavAnimatedVisibilityScope = compositionLocalOf { null } +val LocalSharedTransitionScope = compositionLocalOf { null } + +@Composable +private fun SharedElementScope_CompositionLocal() { + // An example of how to use composition locals to pass around the shared transition scope, far down your UI tree. + // [START_EXCLUDE] + val state = remember { + mutableStateOf(false) + } + // [END_EXCLUDE] + SharedTransitionLayout { + CompositionLocalProvider( + LocalSharedTransitionScope provides this + ) { + // This could also be your top-level NavHost as this provides an AnimatedContentScope + AnimatedContent(state, label = "Top level AnimatedContent") { targetState -> + CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this) { + // Now we can access the scopes in any nested composables as follows: + val sharedTransitionScope = LocalSharedTransitionScope.current + ?: throw IllegalStateException("No SharedElementScope found") + val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current + ?: throw IllegalStateException("No AnimatedVisibility found") + } + // [START_EXCLUDE] + if (targetState.value) { + // do something + } + // [END_EXCLUDE] + } + } + } +} +// [END android_compose_shared_element_scope] + +private object SharedElementScope_Extensions { + // [START android_compose_shared_element_parameters] + @Composable + fun MainContent( + animatedVisibilityScope: AnimatedVisibilityScope, + sharedTransitionScope: SharedTransitionScope + ) { + } + + @Composable + fun Details( + animatedVisibilityScope: AnimatedVisibilityScope, + sharedTransitionScope: SharedTransitionScope + ) { + } + // [END android_compose_shared_element_parameters] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt new file mode 100644 index 000000000..079c68347 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -0,0 +1,732 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.activity.compose.PredictiveBackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.ArcMode +import androidx.compose.animation.core.ExperimentalAnimationSpecApi +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.SeekableTransitionState +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.rememberTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Create +import androidx.compose.material.icons.outlined.Favorite +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material3.Icon +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.example.compose.snippets.R +import com.example.compose.snippets.ui.theme.LavenderLight +import com.example.compose.snippets.ui.theme.RoseLight +import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.launch + +@Preview +@Composable +fun SharedElementApp_BoundsTransformExample() { + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + MainContent( + onShowDetails = { + showDetails = true + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } else { + DetailsContent( + onBack = { + showDetails = false + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } + } + } +} + +@OptIn(ExperimentalAnimationSpecApi::class) +@Composable +private fun MainContent( + onShowDetails: () -> Unit, + modifier: Modifier = Modifier, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope +) { + with(sharedTransitionScope) { + Box(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn( + tween( + boundsAnimationDurationMillis, + easing = FastOutSlowInEasing + ) + ), + exit = fadeOut( + tween( + boundsAnimationDurationMillis, + easing = FastOutSlowInEasing + ) + ), + boundsTransform = boundsTransform + ) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(LavenderLight, RoundedCornerShape(8.dp)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + onShowDetails() + } + .padding(8.dp) + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = boundsTransform + ) + .size(100.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + val textBoundsTransform = BoundsTransform { initialBounds, targetBounds -> + keyframes { + durationMillis = boundsAnimationDurationMillis + initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing + targetBounds at boundsAnimationDurationMillis + } + } + Text( + "Cupcake", fontSize = 21.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = textBoundsTransform + ) + ) + } + } + } +} + +@OptIn(ExperimentalAnimationSpecApi::class) +@Composable +private fun DetailsContent( + modifier: Modifier = Modifier, + onBack: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope +) { + with(sharedTransitionScope) { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn( + tween( + durationMillis = boundsAnimationDurationMillis, + easing = FastOutSlowInEasing + ) + ), + exit = fadeOut( + tween( + durationMillis = boundsAnimationDurationMillis, + easing = FastOutSlowInEasing + ) + ), + boundsTransform = boundsTransform + ) + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(RoseLight, RoundedCornerShape(8.dp)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + onBack() + } + .padding(8.dp) + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = boundsTransform + ) + .size(200.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + // [START android_compose_shared_element_text_bounds_transform] + val textBoundsTransform = BoundsTransform { initialBounds, targetBounds -> + keyframes { + durationMillis = boundsAnimationDurationMillis + initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing + targetBounds at boundsAnimationDurationMillis + } + } + Text( + "Cupcake", fontSize = 28.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = textBoundsTransform + ) + ) + // [END android_compose_shared_element_text_bounds_transform] + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus", + modifier = Modifier.skipToLookaheadSize() + ) + } + } + } +} + +private val boundsTransform = BoundsTransform { _: Rect, _: Rect -> + tween(durationMillis = boundsAnimationDurationMillis, easing = FastOutSlowInEasing) +} +private const val boundsAnimationDurationMillis = 500 + +@Preview +@Composable +private fun SharedElement_Clipping() { + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + Row( + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = this@AnimatedContent + ) + .background(Color.Green.copy(alpha = 0.5f)) + .padding(8.dp) + .clickable { + showDetails = true + } + ) { + // [START android_compose_animations_shared_element_clipping] + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .size(100.dp) + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = this@AnimatedContent + ) + .clip(RoundedCornerShape(16.dp)), + contentScale = ContentScale.Crop + ) + // [END android_compose_animations_shared_element_clipping] + Text( + "Lorem ipsum dolor sit amet.", fontSize = 21.sp, + modifier = Modifier.sharedElement( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = this@AnimatedContent, + + ) + ) + } + } else { + Column( + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = this@AnimatedContent + ) + .background(Color.Green.copy(alpha = 0.7f)) + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .clickable { + showDetails = false + } + + ) { + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .size(200.dp) + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = this@AnimatedContent + ) + .clip(RoundedCornerShape(16.dp)), + contentScale = ContentScale.Crop + ) + Text( + "Lorem ipsum dolor sit amet.", fontSize = 21.sp, + modifier = Modifier.sharedElement( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = this@AnimatedContent + ) + ) + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + ) + } + } + } + } +} + +@Composable +private fun JetsnackBottomBar(modifier: Modifier) { +} + +@Composable +private fun EnterExitJetsnack() { + SharedTransitionLayout { + AnimatedVisibility(visible = true) { + // [START android_compose_shared_element_enter_exit] + JetsnackBottomBar( + modifier = Modifier + .renderInSharedTransitionScopeOverlay( + zIndexInOverlay = 1f, + ) + .animateEnterExit( + enter = fadeIn() + slideInVertically { + it + }, + exit = fadeOut() + slideOutVertically { + it + } + ) + ) + // [END android_compose_shared_element_enter_exit] + } + } +} + +@Preview +@Composable +private fun SharedElement_SkipLookaheadSize() { + // Nested shared bounds sample. + val selectionColor = Color(0xff3367ba) + var expanded by remember { mutableStateOf(true) } + SharedTransitionLayout( + Modifier + .fillMaxSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + expanded = !expanded + } + .background(Color(0x88000000)) + ) { + AnimatedVisibility( + visible = expanded, + enter = EnterTransition.None, + exit = ExitTransition.None + ) { + Box(modifier = Modifier.fillMaxSize()) { + Surface( + Modifier + .align(Alignment.BottomCenter) + .padding(20.dp) + .sharedBounds( + rememberSharedContentState(key = "container"), + this@AnimatedVisibility + ) + .requiredHeightIn(max = 60.dp), + shape = RoundedCornerShape(50), + ) { + Row( + Modifier + .padding(10.dp) + // By using Modifier.skipToLookaheadSize(), we are telling the layout + // system to layout the children of this node as if the animations had + // all finished. This avoid re-laying out the Row with animated width, + // which is _sometimes_ desirable. Try removing this modifier and + // observe the effect. + .skipToLookaheadSize() + ) { + Icon( + Icons.Outlined.Share, + contentDescription = "Share", + modifier = Modifier.padding( + top = 10.dp, + bottom = 10.dp, + start = 10.dp, + end = 20.dp + ) + ) + Icon( + Icons.Outlined.Favorite, + contentDescription = "Favorite", + modifier = Modifier.padding( + top = 10.dp, + bottom = 10.dp, + start = 10.dp, + end = 20.dp + ) + ) + Icon( + Icons.Outlined.Create, + contentDescription = "Create", + tint = Color.White, + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = "icon_background"), + this@AnimatedVisibility + ) + .background(selectionColor, RoundedCornerShape(50)) + .padding( + top = 10.dp, + bottom = 10.dp, + start = 20.dp, + end = 20.dp + ) + .sharedElement( + rememberSharedContentState(key = "icon"), + this@AnimatedVisibility + ) + ) + } + } + } + } + AnimatedVisibility( + visible = !expanded, + enter = EnterTransition.None, + exit = ExitTransition.None + ) { + Box(modifier = Modifier.fillMaxSize()) { + Surface( + Modifier + .align(Alignment.BottomEnd) + .padding(30.dp) + .sharedBounds( + rememberSharedContentState(key = "container"), + this@AnimatedVisibility, + enter = EnterTransition.None, + ) + .sharedBounds( + rememberSharedContentState(key = "icon_background"), + this@AnimatedVisibility, + enter = EnterTransition.None, + exit = ExitTransition.None + ), + shape = RoundedCornerShape(30.dp), + color = selectionColor + ) { + Icon( + Icons.Outlined.Create, + contentDescription = "Create", + tint = Color.White, + modifier = Modifier + .padding(30.dp) + .size(40.dp) + .sharedElement( + rememberSharedContentState(key = "icon"), + this@AnimatedVisibility + ) + ) + } + } + } + } +} + +private val listSnacks = listOf( + Snack("Cupcake", "", R.drawable.cupcake), + Snack("Donut", "", R.drawable.donut), + Snack("Eclair", "", R.drawable.eclair), + Snack("Froyo", "", R.drawable.froyo), + Snack("Gingerbread", "", R.drawable.gingerbread), + Snack("Honeycomb", "", R.drawable.honeycomb), +) + +@Preview +@Composable +fun PlaceholderSizeAnimated_Demo() { + // This demo shows how other items in a layout can respond to shared elements changing in size. + // [START android_compose_shared_element_placeholder_size] + SharedTransitionLayout { + + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = "home" + ) { + composable("home", enterTransition = { fadeIn() }, exitTransition = { fadeOut() }) { + Column(modifier = Modifier.fillMaxSize()) { + Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { + (listSnacks).forEachIndexed { index, snack -> + Image( + painterResource(id = snack.image), + contentDescription = snack.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(8.dp) + .sharedBounds( + rememberSharedContentState(key = "image-${snack.name}"), + animatedVisibilityScope = this@composable, + placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize + ) + .clickable { + navController.navigate("details/$index") + } + .height(180.dp) + .clip(RoundedCornerShape(8.dp)) + .aspectRatio(9f / 16f) + + ) + } + } + Text("Nearby snacks") + Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { + (listSnacks).forEach { snack -> + Image( + painterResource(id = snack.image), + contentDescription = snack.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .height(200.dp) + .aspectRatio(16f / 9f) + .padding(8.dp) + ) + } + } + } + } + composable( + "details/{id}", + arguments = listOf(navArgument("id") { type = NavType.IntType }), + enterTransition = { fadeIn() }, exitTransition = { fadeOut() } + ) { backStackEntry -> + val id = backStackEntry.arguments?.getInt("id") + val snack = listSnacks[id!!] + Column( + Modifier + .fillMaxSize() + .clickable { + navController.navigateUp() + } + ) { + Image( + painterResource(id = snack.image), + contentDescription = snack.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .sharedBounds( + rememberSharedContentState(key = "image-${snack.name}"), + animatedVisibilityScope = this@composable, + placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize + ) + .clip(RoundedCornerShape(8.dp)) + .fillMaxWidth() + .aspectRatio(9f / 16f) + ) + } + } + } + } +// [END android_compose_shared_element_placeholder_size] +} + +private sealed class Screen { + data object Home : Screen() + data class Details(val id: Int) : Screen() +} + +@Preview +@Composable +fun CustomPredictiveBackHandle() { + // [START android_compose_shared_element_custom_seeking] + val seekableTransitionState = remember { + SeekableTransitionState(Screen.Home) + } + val transition = rememberTransition(transitionState = seekableTransitionState) + + PredictiveBackHandler(seekableTransitionState.currentState is Screen.Details) { progress -> + try { + // Whilst a back gesture is in progress, backEvents will be fired for each progress + // update. + progress.collect { backEvent -> + // For each backEvent that comes in, we manually seekTo the reported back progress + try { + seekableTransitionState.seekTo(backEvent.progress, targetState = Screen.Home) + } catch (_: CancellationException) { + // seekTo may be cancelled as expected, if animateTo or subsequent seekTo calls + // before the current seekTo finishes, in this case, we ignore the cancellation. + } + } + // Once collection has completed, we are either fully in the target state, or need + // to progress towards the end. + seekableTransitionState.animateTo(seekableTransitionState.targetState) + } catch (e: CancellationException) { + // When the predictive back gesture is cancelled, we snap to the end state to ensure + // it completes its seeking animation back to the currentState + seekableTransitionState.snapTo(seekableTransitionState.currentState) + throw e + } + } + val coroutineScope = rememberCoroutineScope() + var lastNavigatedIndex by remember { + mutableIntStateOf(0) + } + Column { + Slider( + modifier = Modifier.height(48.dp), + value = seekableTransitionState.fraction, + onValueChange = { + coroutineScope.launch { + if (seekableTransitionState.currentState is Screen.Details) { + seekableTransitionState.seekTo(it, Screen.Home) + } else { + // seek to the previously navigated index + seekableTransitionState.seekTo(it, Screen.Details(lastNavigatedIndex)) + } + } + } + ) + SharedTransitionLayout(modifier = Modifier.weight(1f)) { + transition.AnimatedContent { targetState -> + when (targetState) { + Screen.Home -> { + HomeScreen( + this@SharedTransitionLayout, + this@AnimatedContent, + onItemClick = { + coroutineScope.launch { + lastNavigatedIndex = it + seekableTransitionState.animateTo(Screen.Details(it)) + } + } + ) + } + + is Screen.Details -> { + val snack = listSnacks[targetState.id] + DetailsScreen( + targetState.id, + snack, + this@SharedTransitionLayout, + this@AnimatedContent, + onBackPressed = { + coroutineScope.launch { + seekableTransitionState.animateTo(Screen.Home) + } + } + ) + } + } + } + } + } + + // [END android_compose_shared_element_custom_seeking] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedBoundsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedBoundsSnippets.kt new file mode 100644 index 000000000..3878fa6ad --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedBoundsSnippets.kt @@ -0,0 +1,200 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.R +import com.example.compose.snippets.ui.theme.LavenderLight +import com.example.compose.snippets.ui.theme.RoseLight + +@Preview +@Composable +fun SharedBoundsDemo() { + var showDetails by remember { + mutableStateOf(false) + } + SharedTransitionLayout { + AnimatedContent( + showDetails, + label = "basic_transition" + ) { targetState -> + if (!targetState) { + MainContent( + onShowDetails = { + showDetails = true + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } else { + DetailsContent( + onBack = { + showDetails = false + }, + animatedVisibilityScope = this@AnimatedContent, + sharedTransitionScope = this@SharedTransitionLayout + ) + } + } + } +} + +// [START android_compose_animations_shared_element_shared_bounds] +@Composable +private fun MainContent( + onShowDetails: () -> Unit, + modifier: Modifier = Modifier, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope +) { + with(sharedTransitionScope) { + Row( + modifier = Modifier + .padding(8.dp) + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(), + exit = fadeOut(), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() + ) + // [START_EXCLUDE] + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(LavenderLight, RoundedCornerShape(8.dp)) + .clickable { + onShowDetails() + } + .padding(8.dp) + // [END_EXCLUDE] + ) { + // [START_EXCLUDE] + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(100.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + Text( + "Cupcake", fontSize = 21.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + // [END_EXCLUDE] + } + } +} + +@Composable +private fun DetailsContent( + modifier: Modifier = Modifier, + onBack: () -> Unit, + sharedTransitionScope: SharedTransitionScope, + animatedVisibilityScope: AnimatedVisibilityScope +) { + with(sharedTransitionScope) { + Column( + modifier = Modifier + .padding(top = 200.dp, start = 16.dp, end = 16.dp) + .sharedBounds( + rememberSharedContentState(key = "bounds"), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(), + exit = fadeOut(), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() + ) + // [START_EXCLUDE] + .border(1.dp, Color.Gray.copy(alpha = 0.5f), RoundedCornerShape(8.dp)) + .background(RoseLight, RoundedCornerShape(8.dp)) + .clickable { + onBack() + } + .padding(8.dp) + // [END_EXCLUDE] + + ) { + // [START_EXCLUDE] + Image( + painter = painterResource(id = R.drawable.cupcake), + contentDescription = "Cupcake", + modifier = Modifier + .sharedElement( + rememberSharedContentState(key = "image"), + animatedVisibilityScope = animatedVisibilityScope + ) + .size(200.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + Text( + "Cupcake", fontSize = 28.sp, + modifier = Modifier.sharedBounds( + rememberSharedContentState(key = "title"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + Text( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + ) + // [END_EXCLUDE] + } + } +} +// [END android_compose_animations_shared_element_shared_bounds] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementCommonUseCaseSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementCommonUseCaseSnippets.kt new file mode 100644 index 000000000..944122d1d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementCommonUseCaseSnippets.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest + +@Preview +@Composable +private fun SharedAsyncImage() { + SharedTransitionLayout { + AnimatedVisibility(visible = true) { + // [START android_compose_shared_element_async_image_tip] + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data("your-image-url") + .crossfade(true) + .placeholderMemoryCacheKey("image-key") // same key as shared element key + .memoryCacheKey("image-key") // same key as shared element key + .build(), + placeholder = null, + contentDescription = null, + modifier = Modifier + .size(120.dp) + .sharedBounds( + rememberSharedContentState( + key = "image-key" + ), + animatedVisibilityScope = this + ) + ) + // [END android_compose_shared_element_async_image_tip] + } + } +} + +@Composable +fun debugPlaceholder(@DrawableRes debugPreview: Int) = + if (LocalInspectionMode.current) { + painterResource(id = debugPreview) + } else { + null + } + +@Preview +@Composable +private fun SharedElementTypicalUseText() { + SharedTransitionLayout { + AnimatedVisibility(visible = true) { + // [START android_compose_shared_element_text_tip] + Text( + text = "This is an example of how to share text", + modifier = Modifier + .wrapContentWidth() + .sharedBounds( + rememberSharedContentState( + key = "shared Text" + ), + animatedVisibilityScope = this, + enter = fadeIn(), + exit = fadeOut(), + resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds() + ) + ) + // [END android_compose_shared_element_text_tip] + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt new file mode 100644 index 000000000..cac68a586 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalSharedTransitionApi::class) + +package com.example.compose.snippets.animations.sharedelement + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.example.compose.snippets.R + +private val listSnacks = listOf( + Snack("Cupcake", "", R.drawable.cupcake), + Snack("Donut", "", R.drawable.donut), + Snack("Eclair", "", R.drawable.eclair), + Snack("Froyo", "", R.drawable.froyo), + Snack("Gingerbread", "", R.drawable.gingerbread), + Snack("Honeycomb", "", R.drawable.honeycomb), +) + +// [START android_compose_shared_element_predictive_back] +@Preview +@Composable +fun SharedElement_PredictiveBack() { + SharedTransitionLayout { + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = "home" + ) { + composable("home") { + HomeScreen( + this@SharedTransitionLayout, + this@composable, + { navController.navigate("details/$it") } + ) + } + composable( + "details/{item}", + arguments = listOf(navArgument("item") { type = NavType.IntType }) + ) { backStackEntry -> + val id = backStackEntry.arguments?.getInt("item") + val snack = listSnacks[id!!] + DetailsScreen( + id, + snack, + this@SharedTransitionLayout, + this@composable, + { + navController.navigate("home") + } + ) + } + } + } +} + +@Composable +fun DetailsScreen( + id: Int, + snack: Snack, + sharedTransitionScope: SharedTransitionScope, + animatedContentScope: AnimatedContentScope, + onBackPressed: () -> Unit +) { + with(sharedTransitionScope) { + Column( + Modifier + .fillMaxSize() + .clickable { + onBackPressed() + } + ) { + Image( + painterResource(id = snack.image), + contentDescription = snack.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "image-$id"), + animatedVisibilityScope = animatedContentScope + ) + .aspectRatio(1f) + .fillMaxWidth() + ) + Text( + snack.name, fontSize = 18.sp, + modifier = + Modifier + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "text-$id"), + animatedVisibilityScope = animatedContentScope + ) + .fillMaxWidth() + ) + } + } +} + +@Composable +fun HomeScreen( + sharedTransitionScope: SharedTransitionScope, + animatedContentScope: AnimatedContentScope, + onItemClick: (Int) -> Unit, +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed(listSnacks) { index, item -> + Row( + Modifier.clickable { + onItemClick(index) + } + ) { + Spacer(modifier = Modifier.width(8.dp)) + with(sharedTransitionScope) { + Image( + painterResource(id = item.image), + contentDescription = item.description, + contentScale = ContentScale.Crop, + modifier = Modifier + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "image-$index"), + animatedVisibilityScope = animatedContentScope + ) + .size(100.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + item.name, fontSize = 18.sp, + modifier = Modifier + .align(Alignment.CenterVertically) + .sharedElement( + sharedTransitionScope.rememberSharedContentState(key = "text-$index"), + animatedVisibilityScope = animatedContentScope, + ) + ) + } + } + } + } +} + +data class Snack( + val name: String, + val description: String, + @DrawableRes val image: Int +) +// [END android_compose_shared_element_predictive_back] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureLayering.kt b/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureLayering.kt new file mode 100644 index 000000000..9113a507f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureLayering.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.architecture + +import androidx.compose.animation.Animatable +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color + +@Composable +fun ArchitectureLayering1(condition: Boolean) { + // [START android_compose_architecture_layering1] + val color = animateColorAsState(if (condition) Color.Green else Color.Red) + // [END android_compose_architecture_layering1] +} + +@Composable +fun ArchitectureLayering2(condition: Boolean) { + // [START android_compose_architecture_layering2] + val color = remember { Animatable(Color.Gray) } + LaunchedEffect(condition) { + color.animateTo(if (condition) Color.Green else Color.Red) + } + // [END android_compose_architecture_layering2] +} + +// [START android_compose_architecture_layering3] +@Composable +fun Button( + // … + content: @Composable RowScope.() -> Unit +) { + Surface(/* … */) { + CompositionLocalProvider(/* … */) { // set LocalContentAlpha + ProvideTextStyle(MaterialTheme.typography.button) { + Row( + // … + content = content + ) + } + } + } +} +// [END android_compose_architecture_layering3] + +// [START android_compose_architecture_layering4] +@Composable +fun GradientButton( + // … + background: List, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Row( + // … + modifier = modifier + .clickable(onClick = {}) + .background( + Brush.horizontalGradient(background) + ) + ) { + CompositionLocalProvider(/* … */) { // set material LocalContentAlpha + ProvideTextStyle(MaterialTheme.typography.button) { + content() + } + } + } +} +// [END android_compose_architecture_layering4] + +// [START android_compose_architecture_layering5] +@Composable +fun BespokeButton( + // … + backgroundColor: Color, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Row( + // … + modifier = modifier + .clickable(onClick = {}) + .background(backgroundColor) + ) { + // No Material components used + content() + } +} +// [END android_compose_architecture_layering5] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureSnippets.kt new file mode 100644 index 000000000..3f9f41c3a --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/architecture/ArchitectureSnippets.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.architecture + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +private object ArchitectureSnippets1 { + @Composable + fun ArchitectureSnippets1() { + // [START android_compose_architecture_architecture1] + var name by remember { mutableStateOf("") } + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name") } + ) + // [END android_compose_architecture_architecture1] + } +} + +private object ArchitectureSnippets2 { + data class News(val news: String) + // [START android_compose_architecture_architecture2] + @Composable + fun Header(title: String, subtitle: String) { + // Recomposes when title or subtitle have changed. + } + + @Composable + fun Header(news: News) { + // Recomposes when a new instance of News is passed in. + } + // [END android_compose_architecture_architecture2] +} + +private object ArchitectureSnippets3 { + val localizedString = "" + @OptIn(ExperimentalMaterial3Api::class) + // [START android_compose_architecture_architecture3] + @Composable + fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) { + TopAppBar( + title = { + Text( + text = topAppBarText, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + ) + }, + navigationIcon = { + IconButton(onClick = onBackPressed) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = localizedString + ) + } + }, + // ... + ) + } + // [END android_compose_architecture_architecture3] +} + +private sealed class UiState { + object SignedOut : UiState() + object InProgress : UiState() + object Error : UiState() + object SignIn : UiState() +} + +private object ArchitectureSnippets4 { + // [START android_compose_architecture_architecture4] + + class MyViewModel : ViewModel() { + private val _uiState = mutableStateOf(UiState.SignedOut) + val uiState: State + get() = _uiState + + // ... + } + // [END android_compose_architecture_architecture4] +} + +private object ArchitectureSnippets5 { + // [START android_compose_architecture_architecture5] + class MyViewModel : ViewModel() { + private val _uiState = MutableLiveData(UiState.SignedOut) + val uiState: LiveData + get() = _uiState + + // ... + } + + @Composable + fun MyComposable(viewModel: MyViewModel) { + val uiState = viewModel.uiState.observeAsState() + // ... + } + // [END android_compose_architecture_architecture5] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt new file mode 100644 index 000000000..72b4586a0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt @@ -0,0 +1,554 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.BottomAppBarDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun AppBarExamples( + navigateBack: () -> Unit +) { + var selection by remember { mutableStateOf("none") } + + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + when (selection) { + "bottomBar" -> BottomAppBarExample() + "topBar" -> SmallTopAppBarExample() + "topBarCenter" -> CenterAlignedTopAppBarExample() + "topBarMedium" -> MediumTopAppBarExample() + "topBarLarge" -> LargeTopAppBarExample() + "topBarNavigation" -> TopBarNavigationExample { navigateBack() } + "multiSelection" -> AppBarMultiSelectionExample() + else -> AppBarOptions( + toBottom = { selection = "bottomBar" }, + toTopBarSmall = { selection = "topBar" }, + toTopBarCenter = { selection = "topBarCenter" }, + toTopBarMedium = { selection = "topBarMedium" }, + toTopBarLarge = { selection = "topBarLarge" }, + toTopBarNavigation = { selection = "topBarNavigation" }, + toMultiSelection = { selection = "multiSelection" }, + ) + } + } +} + +@Composable +fun AppBarOptions( + toBottom: () -> Unit, + toTopBarSmall: () -> Unit, + toTopBarCenter: () -> Unit, + toTopBarMedium: () -> Unit, + toTopBarLarge: () -> Unit, + toTopBarNavigation: () -> Unit, + toMultiSelection: () -> Unit, +) { + Column() { + Button({ toBottom() }) { + Text("Bottom bar") + } + Button({ toTopBarSmall() }) { + Text("Small top bar") + } + Button({ toTopBarCenter() }) { + Text("Center aligned top bar") + } + Button({ toTopBarMedium() }) { + Text("Medium top bar") + } + Button({ toTopBarLarge() }) { + Text("Large top bar") + } + Button({ toTopBarNavigation() }) { + Text("Top bar navigation example") + } + Button({ toMultiSelection() }) { + Text("Top bar with multi selection list") + } + } +} + +@Preview +// [START android_compose_components_bottomappbar] +@Composable +fun BottomAppBarExample() { + Scaffold( + bottomBar = { + BottomAppBar( + actions = { + IconButton(onClick = { /* do something */ }) { + Icon(Icons.Filled.Check, contentDescription = "Localized description") + } + IconButton(onClick = { /* do something */ }) { + Icon( + Icons.Filled.Edit, + contentDescription = "Localized description", + ) + } + IconButton(onClick = { /* do something */ }) { + Icon( + Icons.Filled.Mic, + contentDescription = "Localized description", + ) + } + IconButton(onClick = { /* do something */ }) { + Icon( + Icons.Filled.Image, + contentDescription = "Localized description", + ) + } + }, + floatingActionButton = { + FloatingActionButton( + onClick = { /* do something */ }, + containerColor = BottomAppBarDefaults.bottomAppBarFabColor, + elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation() + ) { + Icon(Icons.Filled.Add, "Localized description") + } + } + ) + }, + ) { innerPadding -> + Text( + modifier = Modifier.padding(innerPadding), + text = "Example of a scaffold with a bottom app bar." + ) + } +} +// [END android_compose_components_bottomappbar] + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_components_smalltopappbar] +@Composable +fun SmallTopAppBarExample() { + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text("Small Top App Bar") + } + ) + }, + ) { innerPadding -> + ScrollContent(innerPadding) + } +} +// [END android_compose_components_smalltopappbar] + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_components_centeralignedtopappbar] +@Composable +fun CenterAlignedTopAppBarExample() { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Centered Top App Bar", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + IconButton(onClick = { /* do something */ }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + } + }, + actions = { + IconButton(onClick = { /* do something */ }) { + Icon( + imageVector = Icons.Filled.Menu, + contentDescription = "Localized description" + ) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { innerPadding -> + ScrollContent(innerPadding) + } +} +// [END android_compose_components_centeralignedtopappbar] + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_components_mediumtopappbar] +@Composable +fun MediumTopAppBarExample() { + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + MediumTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Medium Top App Bar", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + IconButton(onClick = { /* do something */ }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + } + }, + actions = { + IconButton(onClick = { /* do something */ }) { + Icon( + imageVector = Icons.Filled.Menu, + contentDescription = "Localized description" + ) + } + }, + scrollBehavior = scrollBehavior + ) + }, + ) { innerPadding -> + ScrollContent(innerPadding) + } +} +// [END android_compose_components_mediumtopappbar] + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_components_largetopappbar] +@Composable +fun LargeTopAppBarExample() { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Large Top App Bar", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + navigationIcon = { + IconButton(onClick = { /* do something */ }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + } + }, + actions = { + IconButton(onClick = { /* do something */ }) { + Icon( + imageVector = Icons.Filled.Menu, + contentDescription = "Localized description" + ) + } + }, + scrollBehavior = scrollBehavior + ) + }, + ) { innerPadding -> + ScrollContent(innerPadding) + } +} +// [END android_compose_components_largetopappbar] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_navigation] +@Composable +fun TopBarNavigationExample( + navigateBack: () -> Unit, +) { + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + "Navigation example", + ) + }, + navigationIcon = { + IconButton(onClick = navigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + } + }, + ) + }, + ) { innerPadding -> + Text( + "Click the back button to pop from the back stack.", + modifier = Modifier.padding(innerPadding), + ) + } +} +// [END android_compose_components_navigation] + +@Composable +fun ScrollContent(innerPadding: PaddingValues) { + val range = 1..100 + + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = innerPadding, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(range.count()) { index -> + Text(text = "- List item number ${index + 1}") + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_appbarselectionactions] +@Composable +fun AppBarSelectionActions( + selectedItems: Set, + modifier: Modifier = Modifier, +) { + val hasSelection = selectedItems.isNotEmpty() + val topBarText = if (hasSelection) { + "Selected ${selectedItems.size} items" + } else { + "List of items" + } + + TopAppBar( + title = { + Text(topBarText) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + actions = { + if (hasSelection) { + IconButton(onClick = { + /* click action */ + }) { + Icon( + imageVector = Icons.Filled.Share, + contentDescription = "Share items" + ) + } + } + }, + modifier = modifier + ) +} +// [END android_compose_components_appbarselectionactions] + +@Preview +@Composable +private fun AppBarSelectionActionsPreview() { + val selectedItems = setOf(1, 2, 3) + + AppBarSelectionActions(selectedItems) +} + +@OptIn(ExperimentalFoundationApi::class) +@Preview +// [START android_compose_components_appbarmultiselectionexample] +@Composable +private fun AppBarMultiSelectionExample( + modifier: Modifier = Modifier, +) { + val listItems by remember { mutableStateOf(listOf(1, 2, 3, 4, 5, 6)) } + var selectedItems by rememberSaveable { mutableStateOf(setOf()) } + + Scaffold( + modifier = modifier, + topBar = { AppBarSelectionActions(selectedItems) } + ) { innerPadding -> + LazyColumn(contentPadding = innerPadding) { + itemsIndexed(listItems) { _, index -> + val isItemSelected = selectedItems.contains(index) + ListItemSelectable( + selected = isItemSelected, + Modifier + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + /* click action */ + }, + onLongClick = { + if (isItemSelected) selectedItems -= index else selectedItems += index + } + ) + ) + } + } + } +} +// [END android_compose_components_appbarmultiselectionexample] + +// [START android_compose_components_listitemselectable] +@Composable +fun ListItemSelectable( + selected: Boolean, + modifier: Modifier = Modifier +) { + Box(modifier = modifier) { + ListItem( + headlineContent = { Text("Long press to select or deselect item") }, + leadingContent = { + if (selected) { + Icon( + Icons.Filled.Check, + contentDescription = "Localized description", + ) + } + } + ) + } +} +// [END android_compose_components_listitemselectable] + +@Preview +@Composable +private fun ListItemSelectablePreview() { + ListItemSelectable(true) +} + +@OptIn(ExperimentalFoundationApi::class) +// [START android_compose_components_lazylistmultiselection +@Composable +fun LazyListMultiSelection( + listItems: List, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), +) { + var selectedItems by rememberSaveable { mutableStateOf(setOf()) } + + LazyColumn(modifier = modifier, + contentPadding = contentPadding) { + itemsIndexed(listItems) { _, index -> + val selected = selectedItems.contains(index) + ListItemSelectable( + selected = selected, + Modifier + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + /* click action */ + }, + onLongClick = { + if (selected) selectedItems -= index else selectedItems += index + } + ) + ) + } + } +} +// [END android_compose_components_lazylistmultiselection + +@Preview +@Composable +private fun LazyListMultiSelectionPreview() { + val listItems = listOf(1, 2, 3) + + LazyListMultiSelection( + listItems, + modifier = Modifier + ) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Badges.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Badges.kt new file mode 100644 index 000000000..895c52def --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Badges.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Mail +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun BadgeExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Minimal badge example", fontWeight = FontWeight.Bold) + BadgeExample() + Text("Badge number example", fontWeight = FontWeight.Bold) + BadgeInteractiveExample() + } +} + +@Preview +// [START android_compose_components_badge] +@Composable +fun BadgeExample() { + BadgedBox( + badge = { + Badge() + } + ) { + Icon( + imageVector = Icons.Filled.Mail, + contentDescription = "Email" + ) + } +} +// [END android_compose_components_badge] + +@Preview +// [START android_compose_components_badgeinteractive] +@Composable +fun BadgeInteractiveExample() { + var itemCount by remember { mutableIntStateOf(0) } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + BadgedBox( + badge = { + if (itemCount > 0) { + Badge( + containerColor = Color.Red, + contentColor = Color.White + ) { + Text("$itemCount") + } + } + } + ) { + Icon( + imageVector = Icons.Filled.ShoppingCart, + contentDescription = "Shopping cart", + ) + } + Button(onClick = { itemCount++ }) { + Text("Add item") + } + } +} +// [END android_compose_components_badgeinteractive] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/BottomSheet.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/BottomSheet.kt new file mode 100644 index 000000000..c7e374312 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/BottomSheet.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_components_partialbottomsheet] +@Composable +fun PartialBottomSheet() { + var showBottomSheet by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = false, + ) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Button( + onClick = { showBottomSheet = true } + ) { + Text("Display partial bottom sheet") + } + + if (showBottomSheet) { + ModalBottomSheet( + modifier = Modifier.fillMaxHeight(), + sheetState = sheetState, + onDismissRequest = { showBottomSheet = false } + ) { + Text( + "Swipe up to open sheet. Swipe down to dismiss.", + modifier = Modifier.padding(16.dp) + ) + } + } + } +} +// [END android_compose_components_partialbottomsheet] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Button.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Button.kt new file mode 100644 index 000000000..1208f816f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Button.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ButtonExamples() { + Column( + modifier = Modifier + .padding(48.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Filled button:") + FilledButtonExample(onClick = { Log.d("Filled button", "Filled button clicked.") }) + Text("Filled tonal button:") + FilledTonalButtonExample(onClick = { Log.d("Filled tonal button", "Filled tonal button clicked.") }) + Text("Elevated button:") + ElevatedButtonExample(onClick = { Log.d("Elevated button", "Elevated button clicked.") }) + Text("Outlined button:") + OutlinedButtonExample(onClick = { Log.d("Outlined button", "Outlined button clicked.") }) + Text("Text button") + TextButtonExample(onClick = { Log.d("Text button", "Text button clicked.") }) + } +} + +// [START android_compose_components_filledbutton] +@Composable +fun FilledButtonExample(onClick: () -> Unit) { + Button(onClick = { onClick() }) { + Text("Filled") + } +} +// [END android_compose_components_filledbutton] + +// [START android_compose_components_filledtonalbutton] +@Composable +fun FilledTonalButtonExample(onClick: () -> Unit) { + FilledTonalButton(onClick = { onClick() }) { + Text("Tonal") + } +} +// [END android_compose_components_filledtonalbutton] + +// [START android_compose_components_elevatedbutton] +@Composable +fun ElevatedButtonExample(onClick: () -> Unit) { + ElevatedButton(onClick = { onClick() }) { + Text("Elevated") + } +} +// [END android_compose_components_elevatedbutton] + +// [START android_compose_components_outlinedbutton] +@Composable +fun OutlinedButtonExample(onClick: () -> Unit) { + OutlinedButton(onClick = { onClick() }) { + Text("Outlined") + } +} +// [END android_compose_components_outlinedbutton] + +// [START android_compose_components_textbutton] +@Composable +fun TextButtonExample(onClick: () -> Unit) { + TextButton( + onClick = { onClick() } + ) { + Text("Text Button") + } +} +// [END android_compose_components_textbutton] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Card.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Card.kt new file mode 100644 index 000000000..1b755819b --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Card.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import android.util.Log +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun CardExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ElevatedCardExample() + FilledCardExample() + OutlinedCardExample() + CustomCardExample { Log.d("custom card", "hello word") } + } +} + +// [START android_compose_components_customcard] +@Composable +fun CustomCardExample(event: () -> Unit) { + Card( + border = BorderStroke(1.dp, Color.Black), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 4.dp + ), + enabled = true, + modifier = Modifier.padding(16.dp), + onClick = event, + shape = RoundedCornerShape(8.dp), + ) { + Text( + text = "Custom card that uses the Card composable. Tap me to say hello world.", + modifier = Modifier + .padding(16.dp) + .fillMaxSize() + ) + } +} +// [END android_compose_components_customcard] + +@Preview +// [START android_compose_components_elevatedcard] +@Composable +fun ElevatedCardExample() { + ElevatedCard( + elevation = CardDefaults.cardElevation( + defaultElevation = 6.dp + ), + modifier = Modifier + .size(width = 240.dp, height = 100.dp) + ) { + Text( + text = "Elevated", + modifier = Modifier + .padding(16.dp), + textAlign = TextAlign.Center, + ) + } +} +// [END android_compose_components_elevatedcard] + +@Preview +// [START android_compose_components_filledcard] +@Composable +fun FilledCardExample() { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + modifier = Modifier + .size(width = 240.dp, height = 100.dp) + ) { + Text( + text = "Filled", + modifier = Modifier + .padding(16.dp), + textAlign = TextAlign.Center, + ) + } +} +// [END android_compose_components_filledcard] + +@Preview +// [START android_compose_components_outlinedcard] +@Composable +fun OutlinedCardExample() { + OutlinedCard( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + border = BorderStroke(1.dp, Color.Black), + modifier = Modifier + .size(width = 240.dp, height = 100.dp) + ) { + Text( + text = "Outlined", + modifier = Modifier + .padding(16.dp), + textAlign = TextAlign.Center, + ) + } +} +// [END android_compose_components_outlinedcard] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt new file mode 100644 index 000000000..fb8a31775 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel +import androidx.compose.material3.carousel.HorizontalUncontainedCarousel +import androidx.compose.material3.carousel.rememberCarouselState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_carousel_multi_browse_basic] +@Composable +fun CarouselExample_MultiBrowse() { + data class CarouselItem( + val id: Int, + @DrawableRes val imageResId: Int, + val contentDescription: String + ) + + val items = remember { + listOf( + CarouselItem(0, R.drawable.cupcake, "cupcake"), + CarouselItem(1, R.drawable.donut, "donut"), + CarouselItem(2, R.drawable.eclair, "eclair"), + CarouselItem(3, R.drawable.froyo, "froyo"), + CarouselItem(4, R.drawable.gingerbread, "gingerbread"), + ) + } + + HorizontalMultiBrowseCarousel( + state = rememberCarouselState { items.count() }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 16.dp, bottom = 16.dp), + preferredItemWidth = 186.dp, + itemSpacing = 8.dp, + contentPadding = PaddingValues(horizontal = 16.dp) + ) { i -> + val item = items[i] + Image( + modifier = Modifier + .height(205.dp) + .maskClip(MaterialTheme.shapes.extraLarge), + painter = painterResource(id = item.imageResId), + contentDescription = item.contentDescription, + contentScale = ContentScale.Crop + ) + } +} +// [END android_compose_carousel_multi_browse_basic] + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_carousel_uncontained_basic] +@Composable +fun CarouselExample() { + data class CarouselItem( + val id: Int, + @DrawableRes val imageResId: Int, + val contentDescription: String + ) + + val carouselItems = remember { + listOf( + CarouselItem(0, R.drawable.cupcake, "cupcake"), + CarouselItem(1, R.drawable.donut, "donut"), + CarouselItem(2, R.drawable.eclair, "eclair"), + CarouselItem(3, R.drawable.froyo, "froyo"), + CarouselItem(4, R.drawable.gingerbread, "gingerbread"), + ) + } + + HorizontalUncontainedCarousel( + state = rememberCarouselState { carouselItems.count() }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 16.dp, bottom = 16.dp), + itemWidth = 186.dp, + itemSpacing = 8.dp, + contentPadding = PaddingValues(horizontal = 16.dp) + ) { i -> + val item = carouselItems[i] + Image( + modifier = Modifier + .height(205.dp) + .maskClip(MaterialTheme.shapes.extraLarge), + painter = painterResource(id = item.imageResId), + contentDescription = item.contentDescription, + contentScale = ContentScale.Crop + ) + } +} +// [END android_compose_carousel_uncontained_basic] + +@Preview +@Composable +fun CarouselExamples() { + Column { + CarouselExample() + CarouselExample_MultiBrowse() + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Checkbox.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Checkbox.kt new file mode 100644 index 000000000..a203914c3 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Checkbox.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.material3.TriStateCheckbox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun CheckboxExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column { + Text("Minimal checkbox example") + CheckboxMinimalExample() + } + Column { + Text("Parent checkbox example") + CheckboxParentExample() + } + } +} + +@Preview +// [START android_compose_components_checkbox_minimal] +@Composable +fun CheckboxMinimalExample() { + var checked by remember { mutableStateOf(true) } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + "Minimal checkbox" + ) + Checkbox( + checked = checked, + onCheckedChange = { checked = it } + ) + } + + Text( + if (checked) "Checkbox is checked" else "Checkbox is unchecked" + ) +} +// [END android_compose_components_checkbox_minimal] + +@Preview +// [START android_compose_components_checkbox_parent] +@Composable +fun CheckboxParentExample() { + // Initialize states for the child checkboxes + val childCheckedStates = remember { mutableStateListOf(false, false, false) } + + // Compute the parent state based on children's states + val parentState = when { + childCheckedStates.all { it } -> ToggleableState.On + childCheckedStates.none { it } -> ToggleableState.Off + else -> ToggleableState.Indeterminate + } + + Column { + // Parent TriStateCheckbox + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Select all") + TriStateCheckbox( + state = parentState, + onClick = { + // Determine new state based on current state + val newState = parentState != ToggleableState.On + childCheckedStates.forEachIndexed { index, _ -> + childCheckedStates[index] = newState + } + } + ) + } + + // Child Checkboxes + childCheckedStates.forEachIndexed { index, checked -> + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Option ${index + 1}") + Checkbox( + checked = checked, + onCheckedChange = { isChecked -> + // Update the individual child state + childCheckedStates[index] = isChecked + } + ) + } + } + } + + if (childCheckedStates.all { it }) { + Text("All options selected") + } +} +// [END android_compose_components_checkbox_parent] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Chip.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Chip.kt new file mode 100644 index 000000000..36533db75 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Chip.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.InputChip +import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun ChipExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Assist chip") + AssistChipExample() + Text("Filter chip") + FilterChipExample() + Text("Input chip") + InputChipExample("Input chip") { Log.d("Input chip", "Chip dismissed") } + Text("Suggestion chip") + SuggestionChipExample() + } +} + +@Preview +// [START android_compose_components_assistchip] +@Composable +fun AssistChipExample() { + AssistChip( + onClick = { Log.d("Assist chip", "hello world") }, + label = { Text("Assist chip") }, + leadingIcon = { + Icon( + Icons.Filled.Settings, + contentDescription = "Localized description", + Modifier.size(AssistChipDefaults.IconSize) + ) + } + ) +} +// [END android_compose_components_assistchip] + +@Preview +// [START android_compose_components_filterchip] +@Composable +fun FilterChipExample() { + var selected by remember { mutableStateOf(false) } + + FilterChip( + onClick = { selected = !selected }, + label = { + Text("Filter chip") + }, + selected = selected, + leadingIcon = if (selected) { + { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = "Done icon", + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + } + } else { + null + }, + ) +} +// [END android_compose_components_filterchip] + +// You could set this up similarly to the filter chip above and have it toggleable, but this is +// an example of a chip that can dismiss with a click. +// [START android_compose_components_inputchip] +@Composable +fun InputChipExample( + text: String, + onDismiss: () -> Unit, +) { + var enabled by remember { mutableStateOf(true) } + if (!enabled) return + + InputChip( + onClick = { + onDismiss() + enabled = !enabled + }, + label = { Text(text) }, + selected = enabled, + avatar = { + Icon( + Icons.Filled.Person, + contentDescription = "Localized description", + Modifier.size(InputChipDefaults.AvatarSize) + ) + }, + trailingIcon = { + Icon( + Icons.Default.Close, + contentDescription = "Localized description", + Modifier.size(InputChipDefaults.AvatarSize) + ) + }, + ) +} +// [END android_compose_components_inputchip] + +@Preview +// [START android_compose_components_suggestionchip] +@Composable +fun SuggestionChipExample() { + SuggestionChip( + onClick = { Log.d("Suggestion chip", "hello world") }, + label = { Text("Suggestion chip") } + ) +} +// [END android_compose_components_suggestionchip] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt new file mode 100644 index 000000000..7f87a5c95 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.navigation.TopComponentsDestination + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ComponentsScreen( + navigate: (TopComponentsDestination) -> Unit +) { + Scaffold(topBar = { + TopAppBar(title = { + Text("Common Components") + }) + }, content = { padding -> + LazyColumn( + modifier = Modifier + .padding(padding) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + items(TopComponentsDestination.entries) { destination -> + NavigationItem(destination) { + navigate( + destination + ) + } + } + } + }) +} + +@Composable +fun NavigationItem(destination: TopComponentsDestination, onClick: () -> Unit) { + ListItem( + headlineContent = { + Text(destination.title) + }, + modifier = Modifier.clickable { + onClick() + } + ) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt new file mode 100644 index 000000000..4d59ea4ef --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt @@ -0,0 +1,370 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material3.Button +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.DateRangePicker +import androidx.compose.material3.DisplayMode +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberDateRangePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import com.example.compose.snippets.ui.theme.SnippetsTheme +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Preview +@Composable +private fun DatePickerPreview() { + SnippetsTheme { + DatePickerExamples() + } +} + +// [START android_compose_components_datepicker_examples] +// [START_EXCLUDE] +@Composable +fun DatePickerExamples() { + var showModal by remember { mutableStateOf(false) } + var showModalInput by remember { mutableStateOf(false) } + var showRangeModal by remember { mutableStateOf(false) } +// [END_EXCLUDE] + var selectedDate by remember { mutableStateOf(null) } +// [START_EXCLUDE] + var selectedDateRange by remember { mutableStateOf>(null to null) } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("Docked date picker:") + DatePickerDocked() + + Text("Open modal picker on click") + DatePickerFieldToModal() + + Text("Modal date pickers:") + Button(onClick = { showModal = true }) { + Text("Show Modal Date Picker") + } + Button(onClick = { showModalInput = true }) { + Text("Show Modal Input Date Picker") + } +// [END_EXCLUDE] + if (selectedDate != null) { + val date = Date(selectedDate!!) + val formattedDate = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(date) + Text("Selected date: $formattedDate") + } else { + Text("No date selected") + } +// [START_EXCLUDE] + + Text("Date range pickers:") + + Button(onClick = { showRangeModal = true }) { + Text("Show Modal Date Range Picker") + } + + if (selectedDateRange.first != null && selectedDateRange.second != null) { + val startDate = Date(selectedDateRange.first!!) + val endDate = Date(selectedDateRange.second!!) + val formattedStartDate = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(startDate) + val formattedEndDate = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(endDate) + Text("Selected date range: $formattedStartDate - $formattedEndDate") + } else { + Text("No date range selected") + } + } + + if (showModal) { +// [END_EXCLUDE] + DatePickerModal( + onDateSelected = { + selectedDate = it + showModal = false + }, + onDismiss = { showModal = false } + ) + } +// [START_EXCLUDE] + + if (showModalInput) { + DatePickerModalInput( + onDateSelected = { + selectedDate = it + showModalInput = false + }, + onDismiss = { showModalInput = false } + ) + } + + if (showRangeModal) { + DateRangePickerModal( + onDateRangeSelected = { + selectedDateRange = it + showRangeModal = false + }, + onDismiss = { showRangeModal = false } + ) + } +} +// [END android_compose_components_datepicker_examples] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_datepicker_modal] +@Composable +fun DatePickerModal( + onDateSelected: (Long?) -> Unit, + onDismiss: () -> Unit +) { + val datePickerState = rememberDatePickerState() + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { + onDateSelected(datePickerState.selectedDateMillis) + onDismiss() + }) { + Text("OK") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) { + DatePicker(state = datePickerState) + } +} +// [END android_compose_components_datepicker_modal] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_datepicker_inputmodal] +@Composable +fun DatePickerModalInput( + onDateSelected: (Long?) -> Unit, + onDismiss: () -> Unit +) { + val datePickerState = rememberDatePickerState(initialDisplayMode = DisplayMode.Input) + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { + onDateSelected(datePickerState.selectedDateMillis) + onDismiss() + }) { + Text("OK") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) { + DatePicker(state = datePickerState) + } +} +// [END android_compose_components_datepicker_inputmodal] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_datepicker_docked] +@Composable +fun DatePickerDocked() { + var showDatePicker by remember { mutableStateOf(false) } + val datePickerState = rememberDatePickerState() + val selectedDate = datePickerState.selectedDateMillis?.let { + convertMillisToDate(it) + } ?: "" + + Box( + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = selectedDate, + onValueChange = { }, + label = { Text("DOB") }, + readOnly = true, + trailingIcon = { + IconButton(onClick = { showDatePicker = !showDatePicker }) { + Icon( + imageVector = Icons.Default.DateRange, + contentDescription = "Select date" + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + ) + + if (showDatePicker) { + Popup( + onDismissRequest = { showDatePicker = false }, + alignment = Alignment.TopStart + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .offset(y = 64.dp) + .shadow(elevation = 4.dp) + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp) + ) { + DatePicker( + state = datePickerState, + showModeToggle = false + ) + } + } + } + } +} + +@Composable +fun DatePickerFieldToModal(modifier: Modifier = Modifier) { + var selectedDate by remember { mutableStateOf(null) } + var showModal by remember { mutableStateOf(false) } + + OutlinedTextField( + value = selectedDate?.let { convertMillisToDate(it) } ?: "", + onValueChange = { }, + label = { Text("DOB") }, + placeholder = { Text("MM/DD/YYYY") }, + trailingIcon = { + Icon(Icons.Default.DateRange, contentDescription = "Select date") + }, + modifier = modifier + .fillMaxWidth() + .pointerInput(selectedDate) { + awaitEachGesture { + // Modifier.clickable doesn't work for text fields, so we use Modifier.pointerInput + // in the Initial pass to observe events before the text field consumes them + // in the Main pass. + awaitFirstDown(pass = PointerEventPass.Initial) + val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial) + if (upEvent != null) { + showModal = true + } + } + } + ) + + if (showModal) { + DatePickerModal( + onDateSelected = { selectedDate = it }, + onDismiss = { showModal = false } + ) + } +} + +fun convertMillisToDate(millis: Long): String { + val formatter = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault()) + return formatter.format(Date(millis)) +} +// [END android_compose_components_datepicker_docked] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_datepicker_range] +@Composable +fun DateRangePickerModal( + onDateRangeSelected: (Pair) -> Unit, + onDismiss: () -> Unit +) { + val dateRangePickerState = rememberDateRangePickerState() + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton( + onClick = { + onDateRangeSelected( + Pair( + dateRangePickerState.selectedStartDateMillis, + dateRangePickerState.selectedEndDateMillis + ) + ) + onDismiss() + } + ) { + Text("OK") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) { + DateRangePicker( + state = dateRangePickerState, + title = { + Text( + text = "Select date range" + ) + }, + showModeToggle = false, + modifier = Modifier + .fillMaxWidth() + .height(500.dp) + .padding(16.dp) + ) + } +} +// [END android_compose_components_datepicker_range] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Dialog.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Dialog.kt new file mode 100644 index 000000000..beb0356b9 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Dialog.kt @@ -0,0 +1,297 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.example.compose.snippets.R + +@Preview +// [START android_compose_components_dialogparent] +@Composable +fun DialogExamples() { + // [START_EXCLUDE] + val openMinimalDialog = remember { mutableStateOf(false) } + val openDialogWithImage = remember { mutableStateOf(false) } + val openFullScreenDialog = remember { mutableStateOf(false) } + // [END_EXCLUDE] + val openAlertDialog = remember { mutableStateOf(false) } + + // [START_EXCLUDE] + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Click the following button to toggle the given dialog example.") + Button( + onClick = { openMinimalDialog.value = !openMinimalDialog.value } + ) { + Text("Minimal dialog component") + } + Button( + onClick = { openDialogWithImage.value = !openDialogWithImage.value } + ) { + Text("Dialog component with an image") + } + Button( + onClick = { openAlertDialog.value = !openAlertDialog.value } + ) { + Text("Alert dialog component with buttons") + } + Button( + onClick = { openFullScreenDialog.value = !openFullScreenDialog.value } + ) { + Text("Full screen dialog") + } + + // [END_EXCLUDE] + when { + // [START_EXCLUDE] + openMinimalDialog.value -> { + MinimalDialog( + onDismissRequest = { openMinimalDialog.value = false }, + ) + } + openDialogWithImage.value -> { + DialogWithImage( + onDismissRequest = { openDialogWithImage.value = false }, + onConfirmation = { + openDialogWithImage.value = false + println("Confirmation registered") // Add logic here to handle confirmation. + }, + painter = painterResource(id = R.drawable.feathertop), + imageDescription = stringResource(id = R.string.feathertop_content_description), + ) + } + openFullScreenDialog.value -> { + FullScreenDialog( + onDismissRequest = { openFullScreenDialog.value = false }, + ) + } + // [END_EXCLUDE] + openAlertDialog.value -> { + AlertDialogExample( + onDismissRequest = { openAlertDialog.value = false }, + onConfirmation = { + openAlertDialog.value = false + println("Confirmation registered") // Add logic here to handle confirmation. + }, + dialogTitle = "Alert dialog example", + dialogText = "This is an example of an alert dialog with buttons.", + icon = Icons.Default.Info + ) + } + } + } +} +// [END android_compose_components_dialogparent] + +// [START android_compose_components_minimaldialog] +@Composable +fun MinimalDialog(onDismissRequest: () -> Unit) { + Dialog(onDismissRequest = { onDismissRequest() }) { + Card( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Text( + text = "This is a minimal dialog", + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center), + textAlign = TextAlign.Center, + ) + } + } +} +// [END android_compose_components_minimaldialog] + +// [START android_compose_components_dialogwithimage] +@Composable +fun DialogWithImage( + onDismissRequest: () -> Unit, + onConfirmation: () -> Unit, + painter: Painter, + imageDescription: String, +) { + Dialog(onDismissRequest = { onDismissRequest() }) { + // Draw a rectangle shape with rounded corners inside the dialog + Card( + modifier = Modifier + .fillMaxWidth() + .height(375.dp) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painter, + contentDescription = imageDescription, + contentScale = ContentScale.Fit, + modifier = Modifier + .height(160.dp) + ) + Text( + text = "This is a dialog with buttons and an image.", + modifier = Modifier.padding(16.dp), + ) + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + TextButton( + onClick = { onDismissRequest() }, + modifier = Modifier.padding(8.dp), + ) { + Text("Dismiss") + } + TextButton( + onClick = { onConfirmation() }, + modifier = Modifier.padding(8.dp), + ) { + Text("Confirm") + } + } + } + } + } +} +// [END android_compose_components_dialogwithimage] + +// [START android_compose_components_alertdialog] +@Composable +fun AlertDialogExample( + onDismissRequest: () -> Unit, + onConfirmation: () -> Unit, + dialogTitle: String, + dialogText: String, + icon: ImageVector, +) { + AlertDialog( + icon = { + Icon(icon, contentDescription = "Example Icon") + }, + title = { + Text(text = dialogTitle) + }, + text = { + Text(text = dialogText) + }, + onDismissRequest = { + onDismissRequest() + }, + confirmButton = { + TextButton( + onClick = { + onConfirmation() + } + ) { + Text("Confirm") + } + }, + dismissButton = { + TextButton( + onClick = { + onDismissRequest() + } + ) { + Text("Dismiss") + } + } + ) +} +// [END android_compose_components_alertdialog] + +// [START android_compose_components_fullscreendialog] +@Composable +fun FullScreenDialog(onDismissRequest: () -> Unit) { + Dialog( + onDismissRequest = { onDismissRequest() }, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true, + ), + ) { + Surface( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceVariant), + ) { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "This is a full screen dialog", + textAlign = TextAlign.Center, + ) + TextButton(onClick = { onDismissRequest() }) { + Text("Dismiss") + } + } + } + } +} +// [END android_compose_components_fullscreendialog] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Divider.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Divider.kt new file mode 100644 index 000000000..e9add3f46 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Divider.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun DividerExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Column with divider", fontWeight = FontWeight.Bold) + HorizontalDividerExample() + Text("Row with divider", fontWeight = FontWeight.Bold) + VerticalDividerExample() + } +} + +@Preview +// [START android_compose_components_horizontaldivider] +@Composable +fun HorizontalDividerExample() { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text("First item in list") + HorizontalDivider(thickness = 2.dp) + Text("Second item in list") + } +} +// [END android_compose_components_horizontaldivider] + +@Preview +// [START android_compose_components_verticaldivider] +@Composable +fun VerticalDividerExample() { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Text("First item in row") + VerticalDivider(color = MaterialTheme.colorScheme.secondary) + Text("Second item in row") + } +} +// [END android_compose_components_verticaldivider] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/FloatingActionButton.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/FloatingActionButton.kt new file mode 100644 index 000000000..61d8074c8 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/FloatingActionButton.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeFloatingActionButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallFloatingActionButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun FloatingActionButtonExamples() { + Column( + modifier = Modifier + .padding(48.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Floating action button:") + Example(onClick = { Log.d("FAB", "FAB clicked.") }) + Text("Small floating action button:") + SmallExample(onClick = { Log.d("FAB", "Small FAB clicked.") }) + Text("Large floating action button:") + LargeExample(onClick = { Log.d("FAB", "Large FAB clicked.") }) + Text("Floating action button with text:") + ExtendedExample(onClick = { Log.d("FAB", "Extended FAB clicked.") }) + } +} + +// [START android_compose_components_fab] +@Composable +fun Example(onClick: () -> Unit) { + FloatingActionButton( + onClick = { onClick() }, + ) { + Icon(Icons.Filled.Add, "Floating action button.") + } +} +// [END android_compose_components_fab] + +// [START android_compose_components_extendedfab] +@Composable +fun ExtendedExample(onClick: () -> Unit) { + ExtendedFloatingActionButton( + onClick = { onClick() }, + icon = { Icon(Icons.Filled.Edit, "Extended floating action button.") }, + text = { Text(text = "Extended FAB") }, + ) +} +// [END android_compose_components_extendedfab] + +// [START android_compose_components_smallfab] +@Composable +fun SmallExample(onClick: () -> Unit) { + SmallFloatingActionButton( + onClick = { onClick() }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.secondary + ) { + Icon(Icons.Filled.Add, "Small floating action button.") + } +} +// [END android_compose_components_smallfab] + +// [START android_compose_components_largefab] +@Composable +fun LargeExample(onClick: () -> Unit) { + LargeFloatingActionButton( + onClick = { onClick() }, + shape = CircleShape, + ) { + Icon(Icons.Filled.Add, "Large floating action button") + } +} +// [END android_compose_components_largefab] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/IconButton.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/IconButton.kt new file mode 100644 index 000000000..59727c71b --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/IconButton.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.example.compose.snippets.R +import kotlinx.coroutines.delay + +// [START android_compose_components_togglebuttonexample] +@Preview +@Composable +fun ToggleIconButtonExample() { + // isToggled initial value should be read from a view model or persistent storage. + var isToggled by rememberSaveable { mutableStateOf(false) } + + IconButton( + onClick = { isToggled = !isToggled } + ) { + Icon( + painter = if (isToggled) painterResource(R.drawable.favorite_filled) else painterResource(R.drawable.favorite), + contentDescription = if (isToggled) "Selected icon button" else "Unselected icon button." + ) + } +} +// [END android_compose_components_togglebuttonexample] + +// [START android_compose_components_iconbutton] +@Composable +fun MomentaryIconButton( + unselectedImage: Int, + selectedImage: Int, + contentDescription: String, + modifier: Modifier = Modifier, + stepDelay: Long = 100L, // Minimum value is 1L milliseconds. + onClick: () -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val pressedListener by rememberUpdatedState(onClick) + + LaunchedEffect(isPressed) { + while (isPressed) { + delay(stepDelay.coerceIn(1L, Long.MAX_VALUE)) + pressedListener() + } + } + + IconButton( + modifier = modifier, + onClick = onClick, + interactionSource = interactionSource + ) { + Icon( + painter = if (isPressed) painterResource(id = selectedImage) else painterResource(id = unselectedImage), + contentDescription = contentDescription, + ) + } +} +// [END android_compose_components_iconbutton] + +// [START android_compose_components_momentaryiconbuttons] +@Preview() +@Composable +fun MomentaryIconButtonExample() { + var pressedCount by remember { mutableIntStateOf(0) } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + MomentaryIconButton( + unselectedImage = R.drawable.fast_rewind, + selectedImage = R.drawable.fast_rewind_filled, + stepDelay = 100L, + onClick = { pressedCount -= 1 }, + contentDescription = "Decrease count button" + ) + Spacer(modifier = Modifier) + Text("advanced by $pressedCount frames") + Spacer(modifier = Modifier) + MomentaryIconButton( + unselectedImage = R.drawable.fast_forward, + selectedImage = R.drawable.fast_forward_filled, + contentDescription = "Increase count button", + stepDelay = 100L, + onClick = { pressedCount += 1 } + ) + } +} +// [END android_compose_components_momentaryiconbuttons] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt new file mode 100644 index 000000000..a3468a7c2 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt @@ -0,0 +1,304 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.DirectionsBike +import androidx.compose.material.icons.automirrored.filled.DirectionsRun +import androidx.compose.material.icons.automirrored.filled.DirectionsWalk +import androidx.compose.material.icons.automirrored.outlined.Help +import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.automirrored.outlined.Send +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Hiking +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.outlined.Feedback +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun MenusExamples() { + var currentExample by remember { mutableStateOf<(@Composable () -> Unit)?>(null) } + + Box(modifier = Modifier.fillMaxSize()) { + currentExample?.let { + it() + FloatingActionButton( + onClick = { currentExample = null }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + ) { + Text(text = "Close example", modifier = Modifier.padding(16.dp)) + } + return + } + + Column(modifier = Modifier.padding(16.dp)) { + Button(onClick = { currentExample = { MinimalDropdownMenu() } }) { + Text("Minimal dropdown menu") + } + Button(onClick = { currentExample = { LongBasicDropdownMenu() } }) { + Text("Dropdown menu with many items") + } + Button(onClick = { currentExample = { DropdownMenuWithDetails() } }) { + Text("Dropdown menu with sections and icons") + } + Button(onClick = { currentExample = { DropdownFilter() } }) { + Text("Menu for applying a filter, attached to a filter chip") + } + } + } +} + +// [START android_compose_components_minimaldropdownmenu] +@Composable +fun MinimalDropdownMenu() { + var expanded by remember { mutableStateOf(false) } + Box( + modifier = Modifier + .padding(16.dp) + ) { + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem( + text = { Text("Option 1") }, + onClick = { /* Do something... */ } + ) + DropdownMenuItem( + text = { Text("Option 2") }, + onClick = { /* Do something... */ } + ) + } + } +} +// [END android_compose_components_minimaldropdownmenu] + +@Preview +@Composable +fun MinimalDropdownMenuPreview() { + MinimalDropdownMenu() +} + +// [START android_compose_components_longbasicdropdownmenu] +@Composable +fun LongBasicDropdownMenu() { + var expanded by remember { mutableStateOf(false) } + // Placeholder list of 100 strings for demonstration + val menuItemData = List(100) { "Option ${it + 1}" } + + Box( + modifier = Modifier + .padding(16.dp) + ) { + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + menuItemData.forEach { option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { /* Do something... */ } + ) + } + } + } +} +// [END android_compose_components_longbasicdropdownmenu] + +@Preview +@Composable +fun LongBasicDropdownMenuPreview() { + LongBasicDropdownMenu() +} + +// [START android_compose_components_dropdownmenuwithdetails] +@Composable +fun DropdownMenuWithDetails() { + var expanded by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + // First section + DropdownMenuItem( + text = { Text("Profile") }, + leadingIcon = { Icon(Icons.Outlined.Person, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + DropdownMenuItem( + text = { Text("Settings") }, + leadingIcon = { Icon(Icons.Outlined.Settings, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + + HorizontalDivider() + + // Second section + DropdownMenuItem( + text = { Text("Send Feedback") }, + leadingIcon = { Icon(Icons.Outlined.Feedback, contentDescription = null) }, + trailingIcon = { Icon(Icons.AutoMirrored.Outlined.Send, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + + HorizontalDivider() + + // Third section + DropdownMenuItem( + text = { Text("About") }, + leadingIcon = { Icon(Icons.Outlined.Info, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + DropdownMenuItem( + text = { Text("Help") }, + leadingIcon = { Icon(Icons.AutoMirrored.Outlined.Help, contentDescription = null) }, + trailingIcon = { Icon(Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + } + } +} +// [END android_compose_components_dropdownmenuwithdetails] + +@Preview +@Composable +fun DropdownMenuWithDetailsPreview() { + DropdownMenuWithDetails() +} + +@Composable +fun DropdownFilter(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .padding(16.dp) + .wrapContentSize(unbounded = true), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.Tune, "Filters") + FilterChip(selected = false, onClick = { /*TODO*/ }, label = { Text("Time") }) + DropdownFilterChip() + FilterChip(selected = false, onClick = { /*TODO*/ }, label = { Text("Wheelchair accessible") }) + } +} + +// [START android_compose_components_dropdownfilterchip] +@Composable +fun DropdownFilterChip(modifier: Modifier = Modifier) { + var isDropdownExpanded by remember { mutableStateOf(false) } + var selectedChipText by remember { mutableStateOf(null) } + Box(modifier) { + FilterChip( + selected = selectedChipText != null, + onClick = { isDropdownExpanded = !isDropdownExpanded }, + label = { Text(if (selectedChipText == null) "Type" else "$selectedChipText") }, + leadingIcon = { if (selectedChipText != null) Icon(Icons.Default.Check, null) }, + trailingIcon = { Icon(Icons.Default.ArrowDropDown, null) }, + ) + DropdownMenu( + expanded = isDropdownExpanded, + onDismissRequest = { isDropdownExpanded = !isDropdownExpanded } + ) { + DropdownMenuItem( + text = { Text("Running") }, + leadingIcon = { Icon(Icons.AutoMirrored.Default.DirectionsRun, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Running") null else "Running" + } + ) + DropdownMenuItem( + text = { Text("Walking") }, + leadingIcon = { Icon(Icons.AutoMirrored.Default.DirectionsWalk, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Walking") null else "Walking" + } + ) + DropdownMenuItem( + text = { Text("Hiking") }, + leadingIcon = { Icon(Icons.Default.Hiking, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Hiking") null else "Hiking" + } + ) + DropdownMenuItem( + text = { Text("Cycling") }, + leadingIcon = { Icon(Icons.AutoMirrored.Default.DirectionsBike, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Cycling") null else "Cycling" + } + ) + } + } +} +// [END android_compose_components_dropdownfilterchip] + +@Preview +@Composable +private fun DropdownFilterPreview() { + DropdownFilter() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Navigation.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Navigation.kt new file mode 100644 index 000000000..9cad68934 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Navigation.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Album +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material.icons.filled.PlaylistAddCircle +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController + +@Composable +fun SongsScreen(modifier: Modifier = Modifier) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Songs Screen") + } +} + +@Composable +fun AlbumScreen(modifier: Modifier = Modifier) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Album Screen") + } +} + +@Composable +fun PlaylistScreen(modifier: Modifier = Modifier) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Playlist Screen") + } +} + +enum class Destination( + val route: String, + val label: String, + val icon: ImageVector, + val contentDescription: String +) { + SONGS("songs", "Songs", Icons.Default.MusicNote, "Songs"), + ALBUM("album", "Album", Icons.Default.Album, "Album"), + PLAYLISTS("playlist", "Playlist", Icons.Default.PlaylistAddCircle, "Playlist") +} + +@Composable +fun AppNavHost( + navController: NavHostController, + startDestination: Destination, + modifier: Modifier = Modifier +) { + NavHost( + navController, + startDestination = startDestination.route + ) { + Destination.entries.forEach { destination -> + composable(destination.route) { + when (destination) { + Destination.SONGS -> SongsScreen() + Destination.ALBUM -> AlbumScreen() + Destination.PLAYLISTS -> PlaylistScreen() + } + } + } + } +} + +@Preview() +// [START android_compose_components_navigationbarexample] +@Composable +fun NavigationBarExample(modifier: Modifier = Modifier) { + val navController = rememberNavController() + val startDestination = Destination.SONGS + var selectedDestination by rememberSaveable { mutableIntStateOf(startDestination.ordinal) } + + Scaffold( + modifier = modifier, + bottomBar = { + NavigationBar(windowInsets = NavigationBarDefaults.windowInsets) { + Destination.entries.forEachIndexed { index, destination -> + NavigationBarItem( + selected = selectedDestination == index, + onClick = { + navController.navigate(route = destination.route) + selectedDestination = index + }, + icon = { + Icon( + destination.icon, + contentDescription = destination.contentDescription + ) + }, + label = { Text(destination.label) } + ) + } + } + } + ) { contentPadding -> + AppNavHost(navController, startDestination, modifier = Modifier.padding(contentPadding)) + } +} +// [END android_compose_components_navigationbarexample] + +@Preview() +// [START android_compose_components_navigationrailexample] +@Composable +fun NavigationRailExample(modifier: Modifier = Modifier) { + val navController = rememberNavController() + val startDestination = Destination.SONGS + var selectedDestination by rememberSaveable { mutableIntStateOf(startDestination.ordinal) } + + Scaffold(modifier = modifier) { contentPadding -> + NavigationRail(modifier = Modifier.padding(contentPadding)) { + Destination.entries.forEachIndexed { index, destination -> + NavigationRailItem( + selected = selectedDestination == index, + onClick = { + navController.navigate(route = destination.route) + selectedDestination = index + }, + icon = { + Icon( + destination.icon, + contentDescription = destination.contentDescription + ) + }, + label = { Text(destination.label) } + ) + } + } + AppNavHost(navController, startDestination) + } +} +// [END android_compose_components_navigationrailexample] + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +// [START android_compose_components_navigationtabexample] +@Composable +fun NavigationTabExample(modifier: Modifier = Modifier) { + val navController = rememberNavController() + val startDestination = Destination.SONGS + var selectedDestination by rememberSaveable { mutableIntStateOf(startDestination.ordinal) } + + Scaffold(modifier = modifier) { contentPadding -> + PrimaryTabRow(selectedTabIndex = selectedDestination, modifier = Modifier.padding(contentPadding)) { + Destination.entries.forEachIndexed { index, destination -> + Tab( + selected = selectedDestination == index, + onClick = { + navController.navigate(route = destination.route) + selectedDestination = index + }, + text = { + Text( + text = destination.label, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + ) + } + } + AppNavHost(navController, startDestination) + } +} +// [END android_compose_components_navigationtabexample] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.kt new file mode 100644 index 000000000..993f1c99a --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Help +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Composable +fun NavigationDrawerExamples() { + // Add more examples here in future if necessary. + + DetailedDrawerExample { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { + Text( + "Swipe from left edge or use menu icon to open the dismissible drawer", + modifier = Modifier.padding(16.dp) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_detaileddrawerexample] +@Composable +fun DetailedDrawerExample( + content: @Composable (PaddingValues) -> Unit +) { + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope = rememberCoroutineScope() + + ModalNavigationDrawer( + drawerContent = { + ModalDrawerSheet { + Column( + modifier = Modifier.padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + Spacer(Modifier.height(12.dp)) + Text("Drawer Title", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleLarge) + HorizontalDivider() + + Text("Section 1", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium) + NavigationDrawerItem( + label = { Text("Item 1") }, + selected = false, + onClick = { /* Handle click */ } + ) + NavigationDrawerItem( + label = { Text("Item 2") }, + selected = false, + onClick = { /* Handle click */ } + ) + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + Text("Section 2", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium) + NavigationDrawerItem( + label = { Text("Settings") }, + selected = false, + icon = { Icon(Icons.Outlined.Settings, contentDescription = null) }, + badge = { Text("20") }, // Placeholder + onClick = { /* Handle click */ } + ) + NavigationDrawerItem( + label = { Text("Help and feedback") }, + selected = false, + icon = { Icon(Icons.AutoMirrored.Outlined.Help, contentDescription = null) }, + onClick = { /* Handle click */ }, + ) + Spacer(Modifier.height(12.dp)) + } + } + }, + drawerState = drawerState + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Navigation Drawer Example") }, + navigationIcon = { + IconButton(onClick = { + scope.launch { + if (drawerState.isClosed) { + drawerState.open() + } else { + drawerState.close() + } + } + }) { + Icon(Icons.Default.Menu, contentDescription = "Menu") + } + } + ) + } + ) { innerPadding -> + content(innerPadding) + } + } +} +// [END android_compose_components_detaileddrawerexample] + +@Preview +@Composable +fun DetailedDrawerExamplePreview() { + NavigationDrawerExamples() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/ProgressIndicator.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/ProgressIndicator.kt new file mode 100644 index 000000000..be18829e2 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/ProgressIndicator.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Preview +@Composable +fun ProgressIndicatorExamples() { + Column( + modifier = Modifier + .padding(48.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Determinate linear indicator:") + LinearDeterminateIndicator() + Text("Indeterminate linear indicator:") + IndeterminateLinearIndicator() + Text("Determinate circular indicator:") + CircularDeterminateIndicator() + Text("Indeterminate circular indicator:") + IndeterminateCircularIndicator() + } +} + +@Preview +// [START android_compose_components_determinateindicator] +@Composable +fun LinearDeterminateIndicator() { + var currentProgress by remember { mutableFloatStateOf(0f) } + var loading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() // Create a coroutine scope + + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + loading = true + scope.launch { + loadProgress { progress -> + currentProgress = progress + } + loading = false // Reset loading when the coroutine finishes + } + }, enabled = !loading) { + Text("Start loading") + } + + if (loading) { + LinearProgressIndicator( + progress = { currentProgress }, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +/** Iterate the progress value */ +suspend fun loadProgress(updateProgress: (Float) -> Unit) { + for (i in 1..100) { + updateProgress(i.toFloat() / 100) + delay(100) + } +} +// [END android_compose_components_determinateindicator] + +@Preview +@Composable +fun CircularDeterminateIndicator() { + var currentProgress by remember { mutableFloatStateOf(0f) } + var loading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() // Create a coroutine scope + + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + loading = true + scope.launch { + loadProgress { progress -> + currentProgress = progress + } + loading = false // Reset loading when the coroutine finishes + } + }, enabled = !loading) { + Text("Start loading") + } + + if (loading) { + CircularProgressIndicator( + progress = { currentProgress }, + modifier = Modifier.width(64.dp), + ) + } + } +} + +@Preview +@Composable +fun IndeterminateLinearIndicator() { + var loading by remember { mutableStateOf(false) } + + Button(onClick = { loading = true }, enabled = !loading) { + Text("Start loading") + } + + if (!loading) return + + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.secondary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) +} + +@Preview +// [START android_compose_components_indeterminateindicator] +@Composable +fun IndeterminateCircularIndicator() { + var loading by remember { mutableStateOf(false) } + + Button(onClick = { loading = true }, enabled = !loading) { + Text("Start loading") + } + + if (!loading) return + + CircularProgressIndicator( + modifier = Modifier.width(64.dp), + color = MaterialTheme.colorScheme.secondary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) +} +// [END android_compose_components_indeterminateindicator] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt new file mode 100644 index 000000000..5dbcb9221 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt @@ -0,0 +1,242 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.PositionalThreshold +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.components.PullToRefreshIndicatorConstants.CROSSFADE_DURATION_MILLIS +import com.example.compose.snippets.components.PullToRefreshIndicatorConstants.SPINNER_SIZE +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private object PullToRefreshIndicatorConstants { + const val CROSSFADE_DURATION_MILLIS = 100 + val SPINNER_SIZE = 16.dp +} + +@Preview +@Composable +fun PullToRefreshBasicPreview() { + PullToRefreshStatefulWrapper { itemCount, isRefreshing, onRefresh -> + val items = List(itemCount) { "Item ${itemCount - it}" } + PullToRefreshBasicSample(items, isRefreshing, onRefresh) + } +} + +@Preview +@Composable +fun PullToRefreshCustomStylePreview() { + PullToRefreshStatefulWrapper { itemCount, isRefreshing, onRefresh -> + val items = List(itemCount) { "Item ${itemCount - it}" } + PullToRefreshCustomStyleSample(items, isRefreshing, onRefresh) + } +} + +@Preview +@Composable +fun PullToRefreshCustomIndicatorPreview() { + PullToRefreshStatefulWrapper { itemCount, isRefreshing, onRefresh -> + val items = List(itemCount) { "Item ${itemCount - it}" } + PullToRefreshCustomIndicatorSample(items, isRefreshing, onRefresh) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_pull_to_refresh_basic] +@Composable +fun PullToRefreshBasicSample( + items: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = modifier + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + ListItem({ Text(text = it) }) + } + } + } +} +// [END android_compose_components_pull_to_refresh_basic] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_pull_to_refresh_custom_style] +@Composable +fun PullToRefreshCustomStyleSample( + items: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + val state = rememberPullToRefreshState() + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = modifier, + state = state, + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = isRefreshing, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = state + ) + }, + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + ListItem({ Text(text = it) }) + } + } + } +} +// [END android_compose_components_pull_to_refresh_custom_style] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_pull_to_refresh_custom_indicator] +@Composable +fun PullToRefreshCustomIndicatorSample( + items: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + val state = rememberPullToRefreshState() + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = modifier, + state = state, + indicator = { + MyCustomIndicator( + state = state, + isRefreshing = isRefreshing, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + ListItem({ Text(text = it) }) + } + } + } +} + +// [START_EXCLUDE] +@OptIn(ExperimentalMaterial3Api::class) +// [END_EXCLUDE] +@Composable +fun MyCustomIndicator( + state: PullToRefreshState, + isRefreshing: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.pullToRefresh( + state = state, + isRefreshing = isRefreshing, + threshold = PositionalThreshold, + onRefresh = { + + } + ), + contentAlignment = Alignment.Center + ) { + Crossfade( + targetState = isRefreshing, + animationSpec = tween(durationMillis = CROSSFADE_DURATION_MILLIS), + modifier = Modifier.align(Alignment.Center) + ) { refreshing -> + if (refreshing) { + CircularProgressIndicator(Modifier.size(SPINNER_SIZE)) + } else { + val distanceFraction = { state.distanceFraction.coerceIn(0f, 1f) } + Icon( + imageVector = Icons.Filled.CloudDownload, + contentDescription = "Refresh", + modifier = Modifier + .size(18.dp) + .graphicsLayer { + val progress = distanceFraction() + this.alpha = progress + this.scaleX = progress + this.scaleY = progress + } + ) + } + } + } +} +// [END android_compose_components_pull_to_refresh_custom_indicator] + +@Composable +fun PullToRefreshStatefulWrapper( + content: @Composable (itemCount: Int, isRefreshing: Boolean, onRefresh: () -> Unit) -> Unit +) { + var itemCount by remember { mutableIntStateOf(15) } + var isRefreshing by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + val onRefresh: () -> Unit = { + isRefreshing = true + coroutineScope.launch { + delay(1500) + itemCount += 5 + isRefreshing = false + } + } + content(itemCount, isRefreshing, onRefresh) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/RadioButton.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/RadioButton.kt new file mode 100644 index 000000000..93961f245 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/RadioButton.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +// [START android_compose_components_radiobuttonsingleselection] +@Composable +fun RadioButtonSingleSelection(modifier: Modifier = Modifier) { + val radioOptions = listOf("Calls", "Missed", "Friends") + val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[0]) } + // Note that Modifier.selectableGroup() is essential to ensure correct accessibility behavior + Column(modifier.selectableGroup()) { + radioOptions.forEach { text -> + Row( + Modifier + .fillMaxWidth() + .height(56.dp) + .selectable( + selected = (text == selectedOption), + onClick = { onOptionSelected(text) }, + role = Role.RadioButton + ) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (text == selectedOption), + onClick = null // null recommended for accessibility with screen readers + ) + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 16.dp) + ) + } + } + } +} +// [END android_compose_components_radiobuttonsingleselection] + +@Preview +@Composable +private fun RadioButtonSingleSelectionPreview() { + RadioButtonSingleSelection() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Scaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Scaffold.kt new file mode 100644 index 000000000..5fe1fb076 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Scaffold.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults.topAppBarColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_components_scaffold] +@Composable +fun ScaffoldExample() { + var presses by remember { mutableIntStateOf(0) } + + Scaffold( + topBar = { + TopAppBar( + colors = topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text("Top app bar") + } + ) + }, + bottomBar = { + BottomAppBar( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.primary, + ) { + Text( + modifier = Modifier + .fillMaxWidth(), + textAlign = TextAlign.Center, + text = "Bottom app bar", + ) + } + }, + floatingActionButton = { + FloatingActionButton(onClick = { presses++ }) { + Icon(Icons.Default.Add, contentDescription = "Add") + } + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + modifier = Modifier.padding(8.dp), + text = + """ + This is an example of a scaffold. It uses the Scaffold composable's parameters to create a screen with a simple top app bar, bottom app bar, and floating action button. + + It also contains some basic inner content, such as this text. + + You have pressed the floating action button $presses times. + """.trimIndent(), + ) + } + } +} +// [END android_compose_components_scaffold] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt new file mode 100644 index 000000000..d97f78524 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt @@ -0,0 +1,299 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun SearchBarExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + var currentExample by remember { mutableStateOf(null) } + + when (currentExample) { + "simple" -> SimpleSearchBarExample() + "fancy" -> CustomizableSearchBarExample() + else -> { + Button(onClick = { currentExample = "simple" }) { + Text("Simple SearchBar") + } + Button(onClick = { currentExample = "fancy" }) { + Text("Customizable SearchBar") + } + } + } + } +} + +// [START android_compose_components_simple_searchbar] +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SimpleSearchBar( + textFieldState: TextFieldState, + onSearch: (String) -> Unit, + searchResults: List, + modifier: Modifier = Modifier +) { + // Controls expansion state of the search bar + var expanded by rememberSaveable { mutableStateOf(false) } + + Box( + modifier + .fillMaxSize() + .semantics { isTraversalGroup = true } + ) { + SearchBar( + modifier = Modifier + .align(Alignment.TopCenter) + .semantics { traversalIndex = 0f }, + inputField = { + SearchBarDefaults.InputField( + query = textFieldState.text.toString(), + onQueryChange = { textFieldState.edit { replace(0, length, it) } }, + onSearch = { + onSearch(textFieldState.text.toString()) + expanded = false + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text("Search") } + ) + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + // Display search results in a scrollable column + Column(Modifier.verticalScroll(rememberScrollState())) { + searchResults.forEach { result -> + ListItem( + headlineContent = { Text(result) }, + modifier = Modifier + .clickable { + textFieldState.edit { replace(0, length, result) } + expanded = false + } + .fillMaxWidth() + ) + } + } + } + } +} +// [END android_compose_components_simple_searchbar] + +@Preview(showBackground = true) +@Composable +private fun SimpleSearchBarExample() { + // Create and remember the text field state + val textFieldState = rememberTextFieldState() + val items = listOf( + "Cupcake", "Donut", "Eclair", "Froyo", "Gingerbread", "Honeycomb", + "Ice Cream Sandwich", "Jelly Bean", "KitKat", "Lollipop" + ) + + // Filter items based on the current search text + val filteredItems by remember { + derivedStateOf { + val searchText = textFieldState.text.toString() + if (searchText.isEmpty()) { + emptyList() + } else { + items.filter { it.contains(searchText, ignoreCase = true) } + } + } + } + + SimpleSearchBar( + textFieldState = textFieldState, + onSearch = { /* Handle search submission */ }, + searchResults = filteredItems + ) +} + +// [START android_compose_components_customizable_searchbar] +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CustomizableSearchBar( + query: String, + onQueryChange: (String) -> Unit, + onSearch: (String) -> Unit, + searchResults: List, + onResultClick: (String) -> Unit, + modifier: Modifier = Modifier, + // Customization options + placeholder: @Composable () -> Unit = { Text("Search") }, + leadingIcon: @Composable (() -> Unit)? = { Icon(Icons.Default.Search, contentDescription = "Search") }, + trailingIcon: @Composable (() -> Unit)? = null, + supportingContent: (@Composable (String) -> Unit)? = null, + leadingContent: (@Composable () -> Unit)? = null, +) { + // Track expanded state of search bar + var expanded by rememberSaveable { mutableStateOf(false) } + + Box( + modifier + .fillMaxSize() + .semantics { isTraversalGroup = true } + ) { + SearchBar( + modifier = Modifier + .align(Alignment.TopCenter) + .semantics { traversalIndex = 0f }, + inputField = { + // Customizable input field implementation + SearchBarDefaults.InputField( + query = query, + onQueryChange = onQueryChange, + onSearch = { + onSearch(query) + expanded = false + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon + ) + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + // Show search results in a lazy column for better performance + LazyColumn { + items(count = searchResults.size) { index -> + val resultText = searchResults[index] + ListItem( + headlineContent = { Text(resultText) }, + supportingContent = supportingContent?.let { { it(resultText) } }, + leadingContent = leadingContent, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + modifier = Modifier + .clickable { + onResultClick(resultText) + expanded = false + } + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + ) + } + } + } + } +} +// [END android_compose_components_customizable_searchbar] + +@Preview(showBackground = true) +@Composable +fun CustomizableSearchBarExample() { + // Manage query state + var query by rememberSaveable { mutableStateOf("") } + val items = listOf( + "Cupcake", "Donut", "Eclair", "Froyo", "Gingerbread", "Honeycomb", + "Ice Cream Sandwich", "Jelly Bean", "KitKat", "Lollipop", "Marshmallow", + "Nougat", "Oreo", "Pie" + ) + + // Filter items based on query + val filteredItems by remember { + derivedStateOf { + if (query.isEmpty()) { + items + } else { + items.filter { it.contains(query, ignoreCase = true) } + } + } + } + + Column(modifier = Modifier.fillMaxSize()) { + CustomizableSearchBar( + query = query, + onQueryChange = { query = it }, + onSearch = { /* Handle search submission */ }, + searchResults = filteredItems, + onResultClick = { query = it }, + // Customize appearance with optional parameters + placeholder = { Text("Search desserts") }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search") }, + trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = "More options") }, + supportingContent = { Text("Android dessert") }, + leadingContent = { Icon(Icons.Filled.Star, contentDescription = "Starred item") } + ) + + // Display the filtered list below the search bar + LazyColumn( + contentPadding = PaddingValues( + start = 16.dp, + top = 72.dp, // Provides space for the search bar + end = 16.dp, + bottom = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.semantics { + traversalIndex = 1f + }, + ) { + items(count = filteredItems.size) { + Text(text = filteredItems[it]) + } + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SegmentedButton.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SegmentedButton.kt new file mode 100644 index 000000000..21fbd9d65 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SegmentedButton.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.DirectionsWalk +import androidx.compose.material.icons.filled.DirectionsBus +import androidx.compose.material.icons.filled.DirectionsCar +import androidx.compose.material3.Icon +import androidx.compose.material3.MultiChoiceSegmentedButtonRow +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun SegmentedButtonExamples() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + + ) { + SingleChoiceSegmentedButton() + Spacer(modifier = Modifier.height(16.dp)) + MultiChoiceSegmentedButton() + } +} + +// [START android_compose_components_singlechoicesegmentedbutton] +@Composable +fun SingleChoiceSegmentedButton(modifier: Modifier = Modifier) { + var selectedIndex by remember { mutableIntStateOf(0) } + val options = listOf("Day", "Month", "Week") + + SingleChoiceSegmentedButtonRow { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = options.size + ), + onClick = { selectedIndex = index }, + selected = index == selectedIndex, + label = { Text(label) } + ) + } + } +} +// [END android_compose_components_singlechoicesegmentedbutton] + +@Preview +@Composable +private fun SingleChoiceSegmentedButtonPreview() { + SingleChoiceSegmentedButton() +} + +// [START android_compose_components_multichoicesegmentedbutton] +@Composable +fun MultiChoiceSegmentedButton(modifier: Modifier = Modifier) { + val selectedOptions = remember { + mutableStateListOf(false, false, false) + } + val options = listOf("Walk", "Ride", "Drive") + + MultiChoiceSegmentedButtonRow { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = options.size + ), + checked = selectedOptions[index], + onCheckedChange = { + selectedOptions[index] = !selectedOptions[index] + }, + icon = { SegmentedButtonDefaults.Icon(selectedOptions[index]) }, + label = { + when (label) { + "Walk" -> Icon( + imageVector = + Icons.AutoMirrored.Filled.DirectionsWalk, + contentDescription = "Directions Walk" + ) + "Ride" -> Icon( + imageVector = + Icons.Default.DirectionsBus, + contentDescription = "Directions Bus" + ) + "Drive" -> Icon( + imageVector = + Icons.Default.DirectionsCar, + contentDescription = "Directions Car" + ) + } + } + ) + } + } +} +// [END android_compose_components_multichoicesegmentedbutton] + +@Preview +@Composable +private fun MultiChoiceSegmentedButtonPreview() { + MultiChoiceSegmentedButton() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Slider.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Slider.kt new file mode 100644 index 000000000..39165b63b --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Slider.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RangeSlider +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun SliderExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Minimal slider component") + SliderMinimalExample() + Text("Advanced slider component") + SliderAdvancedExample() + Text("Range slider component") + RangeSliderExample() + } +} + +// [START android_compose_components_sliderminimal] +@Preview +@Composable +fun SliderMinimalExample() { + var sliderPosition by remember { mutableFloatStateOf(0f) } + Column { + Slider( + value = sliderPosition, + onValueChange = { sliderPosition = it } + ) + Text(text = sliderPosition.toString()) + } +} +// [END android_compose_components_sliderminimal] + +// [START android_compose_components_slideradvanced] +@Preview +@Composable +fun SliderAdvancedExample() { + var sliderPosition by remember { mutableFloatStateOf(0f) } + Column { + Slider( + value = sliderPosition, + onValueChange = { sliderPosition = it }, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.secondary, + activeTrackColor = MaterialTheme.colorScheme.secondary, + inactiveTrackColor = MaterialTheme.colorScheme.secondaryContainer, + ), + steps = 3, + valueRange = 0f..50f + ) + Text(text = sliderPosition.toString()) + } +} +// [END android_compose_components_slideradvanced] + +// [START android_compose_components_rangeslider] +@Preview +@Composable +fun RangeSliderExample() { + var sliderPosition by remember { mutableStateOf(0f..100f) } + Column { + RangeSlider( + value = sliderPosition, + steps = 5, + onValueChange = { range -> sliderPosition = range }, + valueRange = 0f..100f, + onValueChangeFinished = { + // launch some business logic update with the state you hold + // viewModel.updateSelectedSliderValue(sliderPosition) + }, + ) + Text(text = sliderPosition.toString()) + } +} +// [END android_compose_components_rangeslider] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt new file mode 100644 index 000000000..22dc99303 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckBox +import androidx.compose.material.icons.filled.CheckBoxOutlineBlank +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart +import androidx.compose.material3.SwipeToDismissBoxValue.Settled +import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd +import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun SwipeToDismissBoxExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Swipe to dismiss with change of background", fontWeight = FontWeight.Bold) + SwipeItemExample() + Text("Swipe to dismiss with a cross-fade animation", fontWeight = FontWeight.Bold) + SwipeItemWithAnimationExample() + } +} + +// [START android_compose_components_todoitem] +data class TodoItem( + val itemDescription: String, + var isItemDone: Boolean = false +) +// [END android_compose_components_todoitem] + +// [START android_compose_components_swipeitem] +@Composable +fun TodoListItem( + todoItem: TodoItem, + onToggleDone: (TodoItem) -> Unit, + onRemove: (TodoItem) -> Unit, + modifier: Modifier = Modifier, +) { + val swipeToDismissBoxState = rememberSwipeToDismissBoxState( + confirmValueChange = { + if (it == StartToEnd) onToggleDone(todoItem) + else if (it == EndToStart) onRemove(todoItem) + // Reset item when toggling done status + it != StartToEnd + } + ) + + SwipeToDismissBox( + state = swipeToDismissBoxState, + modifier = modifier.fillMaxSize(), + backgroundContent = { + when (swipeToDismissBoxState.dismissDirection) { + StartToEnd -> { + Icon( + if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank, + contentDescription = if (todoItem.isItemDone) "Done" else "Not done", + modifier = Modifier + .fillMaxSize() + .background(Color.Blue) + .wrapContentSize(Alignment.CenterStart) + .padding(12.dp), + tint = Color.White + ) + } + EndToStart -> { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Remove item", + modifier = Modifier + .fillMaxSize() + .background(Color.Red) + .wrapContentSize(Alignment.CenterEnd) + .padding(12.dp), + tint = Color.White + ) + } + Settled -> {} + } + } + ) { + ListItem( + headlineContent = { Text(todoItem.itemDescription) }, + supportingContent = { Text("swipe me to update or remove.") } + ) + } +} +// [END android_compose_components_swipeitem] + +@Preview(showBackground = true) +// [START android_compose_components_swipeitemexample] +@Composable +private fun SwipeItemExample() { + val todoItems = remember { + mutableStateListOf( + TodoItem("Pay bills"), TodoItem("Buy groceries"), + TodoItem("Go to gym"), TodoItem("Get dinner") + ) + } + + LazyColumn { + items( + items = todoItems, + key = { it.itemDescription } + ) { todoItem -> + TodoListItem( + todoItem = todoItem, + onToggleDone = { todoItem -> + todoItem.isItemDone = !todoItem.isItemDone + }, + onRemove = { todoItem -> + todoItems -= todoItem + }, + modifier = Modifier.animateItem() + ) + } + } +} +// [END android_compose_components_swipeitemexample] + +// [START android_compose_components_swipecarditem] +@Composable +fun TodoListItemWithAnimation( + todoItem: TodoItem, + onToggleDone: (TodoItem) -> Unit, + onRemove: (TodoItem) -> Unit, + modifier: Modifier = Modifier, +) { + val swipeToDismissBoxState = rememberSwipeToDismissBoxState( + confirmValueChange = { + if (it == StartToEnd) onToggleDone(todoItem) + else if (it == EndToStart) onRemove(todoItem) + // Reset item when toggling done status + it != StartToEnd + } + ) + + SwipeToDismissBox( + state = swipeToDismissBoxState, + modifier = modifier.fillMaxSize(), + backgroundContent = { + when (swipeToDismissBoxState.dismissDirection) { + StartToEnd -> { + Icon( + if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank, + contentDescription = if (todoItem.isItemDone) "Done" else "Not done", + modifier = Modifier + .fillMaxSize() + .drawBehind { + drawRect(lerp(Color.LightGray, Color.Blue, swipeToDismissBoxState.progress)) + } + .wrapContentSize(Alignment.CenterStart) + .padding(12.dp), + tint = Color.White + ) + } + EndToStart -> { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Remove item", + modifier = Modifier + .fillMaxSize() + .background(lerp(Color.LightGray, Color.Red, swipeToDismissBoxState.progress)) + .wrapContentSize(Alignment.CenterEnd) + .padding(12.dp), + tint = Color.White + ) + } + Settled -> {} + } + } + ) { + OutlinedCard(shape = RectangleShape) { + ListItem( + headlineContent = { Text(todoItem.itemDescription) }, + supportingContent = { Text("swipe me to update or remove.") } + ) + } + } +} +// [END android_compose_components_swipecarditem] + +@Preview +// [START android_compose_components_swipecarditemexample] +@Composable +private fun SwipeItemWithAnimationExample() { + val todoItems = remember { + mutableStateListOf( + TodoItem("Pay bills"), TodoItem("Buy groceries"), + TodoItem("Go to gym"), TodoItem("Get dinner") + ) + } + + LazyColumn { + items( + items = todoItems, + key = { it.itemDescription } + ) { todoItem -> + TodoListItemWithAnimation( + todoItem = todoItem, + onToggleDone = { todoItem -> + todoItem.isItemDone = !todoItem.isItemDone + }, + onRemove = { todoItem -> + todoItems -= todoItem + }, + modifier = Modifier.animateItem() + ) + } + } +} +// [END android_compose_components_swipecarditemexample] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Switch.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Switch.kt new file mode 100644 index 000000000..25a60c65a --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Switch.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun SwitchExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Minimal switch component") + SwitchMinimalExample() + Text("Switch with label") + SwitchWithLabelMinimalExample() + Text("Switch with icon") + SwitchWithIconExample() + Text("Switch with custom colors") + SwitchWithCustomColors() + } +} + +@Preview +// [START android_compose_components_switchminimal] +@Composable +fun SwitchMinimalExample() { + var checked by remember { mutableStateOf(true) } + + Switch( + checked = checked, + onCheckedChange = { + checked = it + } + ) +} +// [END android_compose_components_switchminimal] + +@Preview +// [START android_compose_components_switchwithicon] +@Composable +fun SwitchWithIconExample() { + var checked by remember { mutableStateOf(true) } + + Switch( + checked = checked, + onCheckedChange = { + checked = it + }, + thumbContent = if (checked) { + { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } + } else { + null + } + ) +} +// [END android_compose_components_switchwithicon] + +@Preview +// [START android_compose_components_switchwithlabel] +@Composable +fun SwitchWithLabelMinimalExample() { + var checked by remember { mutableStateOf(true) } + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.padding(8.dp), + text = if (checked) "Checked" else "Unchecked", + ) + Switch( + checked = checked, + onCheckedChange = { + checked = it + } + ) + } +} +// [END android_compose_components_switchwithlabel] + +@Preview +// [START android_compose_components_switchwithcustomcolors] +@Composable +fun SwitchWithCustomColors() { + var checked by remember { mutableStateOf(true) } + + Switch( + checked = checked, + onCheckedChange = { + checked = it + }, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.primary, + checkedTrackColor = MaterialTheme.colorScheme.primaryContainer, + uncheckedThumbColor = MaterialTheme.colorScheme.secondary, + uncheckedTrackColor = MaterialTheme.colorScheme.secondaryContainer, + ) + ) +} +// [END android_compose_components_switchwithcustomcolors] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/TimePickers.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/TimePickers.kt new file mode 100644 index 000000000..08f3c7aad --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/TimePickers.kt @@ -0,0 +1,440 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessTime +import androidx.compose.material.icons.filled.EditCalendar +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TimeInput +import androidx.compose.material3.TimePicker +import androidx.compose.material3.TimePickerState +import androidx.compose.material3.rememberTimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TimePickerExamples() { + var showMenu by remember { mutableStateOf(true) } + + var showDialExample by remember { mutableStateOf(false) } + var showInputExample by remember { mutableStateOf(false) } + var showDialWithDialogExample by remember { mutableStateOf(false) } + var showAdvancedExample by remember { mutableStateOf(false) } + + var selectedTime: TimePickerState? by remember { mutableStateOf(null) } + + val formatter = remember { SimpleDateFormat("hh:mm a", Locale.getDefault()) } + + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (showMenu) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Button(onClick = { + showDialExample = true + showMenu = false + }) { + Text("Dial time picker") + } + Button(onClick = { + showInputExample = true + showMenu = false + }) { + Text("Input time picker") + } + Button(onClick = { + showDialWithDialogExample = true + showMenu = false + }) { + Text("Time picker with dialog") + } + Button(onClick = { + showAdvancedExample = true + showMenu = false + }) { + Text("Time picker with custom dialog") + } + if (selectedTime != null) { + val cal = Calendar.getInstance() + cal.set(Calendar.HOUR_OF_DAY, selectedTime!!.hour) + cal.set(Calendar.MINUTE, selectedTime!!.minute) + cal.isLenient = false + Text("Selected time = ${formatter.format(cal.time)}") + } else { + Text("No time selected.") + } + } + } + + when { + showDialExample -> DialUseStateExample( + onDismiss = { + showDialExample = false + showMenu = true + }, + onConfirm = { + time -> + selectedTime = time + showDialExample = false + showMenu = true + }, + ) + showInputExample -> InputUseStateExample( + onDismiss = { + showInputExample = false + showMenu = true + }, + onConfirm = { + time -> + selectedTime = time + showInputExample = false + showMenu = true + }, + ) + showDialWithDialogExample -> DialWithDialogExample( + onDismiss = { + showDialWithDialogExample = false + showMenu = true + }, + onConfirm = { + time -> + selectedTime = time + showDialWithDialogExample = false + showMenu = true + }, + ) + showAdvancedExample -> AdvancedTimePickerExample( + onDismiss = { + showAdvancedExample = false + showMenu = true + }, + onConfirm = { + time -> + selectedTime = time + showAdvancedExample = false + showMenu = true + }, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_dial] +@Composable +fun DialExample( + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + val currentTime = Calendar.getInstance() + + val timePickerState = rememberTimePickerState( + initialHour = currentTime.get(Calendar.HOUR_OF_DAY), + initialMinute = currentTime.get(Calendar.MINUTE), + is24Hour = true, + ) + + Column { + TimePicker( + state = timePickerState, + ) + Button(onClick = onDismiss) { + Text("Dismiss picker") + } + Button(onClick = onConfirm) { + Text("Confirm selection") + } + } +} +// [END android_compose_components_dial] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_input] +@Composable +fun InputExample( + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + val currentTime = Calendar.getInstance() + + val timePickerState = rememberTimePickerState( + initialHour = currentTime.get(Calendar.HOUR_OF_DAY), + initialMinute = currentTime.get(Calendar.MINUTE), + is24Hour = true, + ) + + Column { + TimeInput( + state = timePickerState, + ) + Button(onClick = onDismiss) { + Text("Dismiss picker") + } + Button(onClick = onConfirm) { + Text("Confirm selection") + } + } +} +// [END android_compose_components_input] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_dial_usestate] +@Composable +fun DialUseStateExample( + onConfirm: (TimePickerState) -> Unit, + onDismiss: () -> Unit, +) { + val currentTime = Calendar.getInstance() + + val timePickerState = rememberTimePickerState( + initialHour = currentTime.get(Calendar.HOUR_OF_DAY), + initialMinute = currentTime.get(Calendar.MINUTE), + is24Hour = true, + ) + + Column { + TimePicker( + state = timePickerState, + ) + Button(onClick = onDismiss) { + Text("Dismiss picker") + } + Button(onClick = { onConfirm(timePickerState) }) { + Text("Confirm selection") + } + } +} +// [END android_compose_components_dial_usestate] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_input_usestate] +@Composable +fun InputUseStateExample( + onConfirm: (TimePickerState) -> Unit, + onDismiss: () -> Unit, +) { + val currentTime = Calendar.getInstance() + + val timePickerState = rememberTimePickerState( + initialHour = currentTime.get(Calendar.HOUR_OF_DAY), + initialMinute = currentTime.get(Calendar.MINUTE), + is24Hour = true, + ) + + Column { + TimeInput( + state = timePickerState, + ) + Button(onClick = onDismiss) { + Text("Dismiss picker") + } + Button(onClick = { onConfirm(timePickerState) }) { + Text("Confirm selection") + } + } +} +// [END android_compose_components_input_usestate] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_timepickerdialog] +@Composable +fun DialWithDialogExample( + onConfirm: (TimePickerState) -> Unit, + onDismiss: () -> Unit, +) { + val currentTime = Calendar.getInstance() + + val timePickerState = rememberTimePickerState( + initialHour = currentTime.get(Calendar.HOUR_OF_DAY), + initialMinute = currentTime.get(Calendar.MINUTE), + is24Hour = true, + ) + + TimePickerDialog( + onDismiss = { onDismiss() }, + onConfirm = { onConfirm(timePickerState) } + ) { + TimePicker( + state = timePickerState, + ) + } +} + +@Composable +fun TimePickerDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit, + content: @Composable () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + dismissButton = { + TextButton(onClick = { onDismiss() }) { + Text("Dismiss") + } + }, + confirmButton = { + TextButton(onClick = { onConfirm() }) { + Text("OK") + } + }, + text = { content() } + ) +} +// [END android_compose_components_timepickerdialog] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_advanced] +@Composable +fun AdvancedTimePickerExample( + onConfirm: (TimePickerState) -> Unit, + onDismiss: () -> Unit, +) { + + val currentTime = Calendar.getInstance() + + val timePickerState = rememberTimePickerState( + initialHour = currentTime.get(Calendar.HOUR_OF_DAY), + initialMinute = currentTime.get(Calendar.MINUTE), + is24Hour = true, + ) + + /** Determines whether the time picker is dial or input */ + var showDial by remember { mutableStateOf(true) } + + /** The icon used for the icon button that switches from dial to input */ + val toggleIcon = if (showDial) { + Icons.Filled.EditCalendar + } else { + Icons.Filled.AccessTime + } + + AdvancedTimePickerDialog( + onDismiss = { onDismiss() }, + onConfirm = { onConfirm(timePickerState) }, + toggle = { + IconButton(onClick = { showDial = !showDial }) { + Icon( + imageVector = toggleIcon, + contentDescription = "Time picker type toggle", + ) + } + }, + ) { + if (showDial) { + TimePicker( + state = timePickerState, + ) + } else { + TimeInput( + state = timePickerState, + ) + } + } +} + +@Composable +fun AdvancedTimePickerDialog( + title: String = "Select Time", + onDismiss: () -> Unit, + onConfirm: () -> Unit, + toggle: @Composable () -> Unit = {}, + content: @Composable () -> Unit, +) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface( + shape = MaterialTheme.shapes.extraLarge, + tonalElevation = 6.dp, + modifier = + Modifier + .width(IntrinsicSize.Min) + .height(IntrinsicSize.Min) + .background( + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surface + ), + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 20.dp), + text = title, + style = MaterialTheme.typography.labelMedium + ) + content() + Row( + modifier = Modifier + .height(40.dp) + .fillMaxWidth() + ) { + toggle() + Spacer(modifier = Modifier.weight(1f)) + TextButton(onClick = onDismiss) { Text("Cancel") } + TextButton(onClick = onConfirm) { Text("OK") } + } + } + } + } +} +// [END android_compose_components_advanced] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt new file mode 100644 index 000000000..9a034e261 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Camera +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.RichTooltip +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Composable +fun TooltipExamples() { + Text( + "Long press an icon to see the tooltip.", + modifier = Modifier.fillMaxWidth().padding(16.dp), + textAlign = TextAlign.Center + ) + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + PlainTooltipExample() + RichTooltipExample() + AdvancedRichTooltipExample() + } +} + +@Preview +@Composable +private fun TooltipExamplesPreview() { + TooltipExamples() +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_plaintooltipexample] +@Composable +fun PlainTooltipExample( + modifier: Modifier = Modifier, + plainTooltipText: String = "Add to favorites" +) { + TooltipBox( + modifier = modifier, + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip { Text(plainTooltipText) } + }, + state = rememberTooltipState() + ) { + IconButton(onClick = { /* Do something... */ }) { + Icon( + imageVector = Icons.Filled.Favorite, + contentDescription = "Add to favorites" + ) + } + } +} + +// [END android_compose_components_plaintooltipexample] + +@Preview +@Composable +private fun PlainTooltipSamplePreview() { + PlainTooltipExample() +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_richtooltipexample] +@Composable +fun RichTooltipExample( + modifier: Modifier = Modifier, + richTooltipSubheadText: String = "Rich Tooltip", + richTooltipText: String = "Rich tooltips support multiple lines of informational text." +) { + TooltipBox( + modifier = modifier, + positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(), + tooltip = { + RichTooltip( + title = { Text(richTooltipSubheadText) } + ) { + Text(richTooltipText) + } + }, + state = rememberTooltipState() + ) { + IconButton(onClick = { /* Icon button's click event */ }) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = "Show more information" + ) + } + } +} +// [END android_compose_components_richtooltipexample] + +@Preview +@Composable +private fun RichTooltipSamplePreview() { + RichTooltipExample() +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_advancedrichtooltipexample] +@Composable +fun AdvancedRichTooltipExample( + modifier: Modifier = Modifier, + richTooltipSubheadText: String = "Custom Rich Tooltip", + richTooltipText: String = "Rich tooltips support multiple lines of informational text.", + richTooltipActionText: String = "Dismiss" +) { + val tooltipState = rememberTooltipState() + val coroutineScope = rememberCoroutineScope() + + TooltipBox( + modifier = modifier, + positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(), + tooltip = { + RichTooltip( + title = { Text(richTooltipSubheadText) }, + action = { + Row { + TextButton(onClick = { + coroutineScope.launch { + tooltipState.dismiss() + } + }) { + Text(richTooltipActionText) + } + } + }, + ) { + Text(richTooltipText) + } + }, + state = tooltipState + ) { + IconButton(onClick = { + coroutineScope.launch { + tooltipState.show() + } + }) { + Icon( + imageVector = Icons.Filled.Camera, + contentDescription = "Open camera" + ) + } + } +} +// [END android_compose_components_advancedrichtooltipexample] + +@Preview +@Composable +private fun RichTooltipWithCustomCaretSamplePreview() { + AdvancedRichTooltipExample() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/CustomDesignSystem.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/CustomDesignSystem.kt new file mode 100644 index 000000000..8b42ab1b8 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/CustomDesignSystem.kt @@ -0,0 +1,352 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.designsystems + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.shape.ZeroCornerSize +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Shapes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +private object CustomDesignSystemExtend { + // [START android_compose_designsystems_custom_extend] + + // Use with MaterialTheme.colorScheme.snackbarAction + val ColorScheme.snackbarAction: Color + @Composable + get() = if (isSystemInDarkTheme()) Red300 else Red700 + + // Use with MaterialTheme.typography.textFieldInput + val Typography.textFieldInput: TextStyle + get() = TextStyle(/* ... */) + + // Use with MaterialTheme.shapes.card + val Shapes.card: Shape + get() = RoundedCornerShape(size = 20.dp) + // [END android_compose_designsystems_custom_extend] + + val Red300 = Color(0xFFE57373) + val Red700 = Color(0xFFD32F2F) +} + +private object CustomDesignSystemExtendTheme { + // [START android_compose_designsystems_custom_extend_theme] + @Immutable + data class ExtendedColors( + val caution: Color, + val onCaution: Color + ) + + val LocalExtendedColors = staticCompositionLocalOf { + ExtendedColors( + caution = Color.Unspecified, + onCaution = Color.Unspecified + ) + } + + @Composable + fun ExtendedTheme( + /* ... */ + content: @Composable () -> Unit + ) { + val extendedColors = ExtendedColors( + caution = Color(0xFFFFCC02), + onCaution = Color(0xFF2C2D30) + ) + CompositionLocalProvider(LocalExtendedColors provides extendedColors) { + MaterialTheme( + /* colors = ..., typography = ..., shapes = ... */ + content = content + ) + } + } + + // Use with eg. ExtendedTheme.colors.caution + object ExtendedTheme { + val colors: ExtendedColors + @Composable + get() = LocalExtendedColors.current + } + // [END android_compose_designsystems_custom_extend_theme] + + // [START android_compose_designsystems_extend_button] + @Composable + fun ExtendedButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit + ) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = ExtendedTheme.colors.caution, + contentColor = ExtendedTheme.colors.onCaution + /* Other colors use values from MaterialTheme */ + ), + onClick = onClick, + modifier = modifier, + content = content + ) + } + // [END android_compose_designsystems_extend_button] + + // [START android_compose_designsystems_use_button] + @Composable + fun ExtendedApp() { + ExtendedTheme { + /*...*/ + ExtendedButton(onClick = { /* ... */ }) { + /* ... */ + } + } + } + // [END android_compose_designsystems_use_button] +} + +object ReplaceMaterialSystem { + // [START android_compose_designsystems_replace_material] + @Immutable + data class ReplacementTypography( + val body: TextStyle, + val title: TextStyle + ) + + @Immutable + data class ReplacementShapes( + val component: Shape, + val surface: Shape + ) + + val LocalReplacementTypography = staticCompositionLocalOf { + ReplacementTypography( + body = TextStyle.Default, + title = TextStyle.Default + ) + } + val LocalReplacementShapes = staticCompositionLocalOf { + ReplacementShapes( + component = RoundedCornerShape(ZeroCornerSize), + surface = RoundedCornerShape(ZeroCornerSize) + ) + } + + @Composable + fun ReplacementTheme( + /* ... */ + content: @Composable () -> Unit + ) { + val replacementTypography = ReplacementTypography( + body = TextStyle(fontSize = 16.sp), + title = TextStyle(fontSize = 32.sp) + ) + val replacementShapes = ReplacementShapes( + component = RoundedCornerShape(percent = 50), + surface = RoundedCornerShape(size = 40.dp) + ) + CompositionLocalProvider( + LocalReplacementTypography provides replacementTypography, + LocalReplacementShapes provides replacementShapes + ) { + MaterialTheme( + /* colors = ... */ + content = content + ) + } + } + + // Use with eg. ReplacementTheme.typography.body + object ReplacementTheme { + val typography: ReplacementTypography + @Composable + get() = LocalReplacementTypography.current + val shapes: ReplacementShapes + @Composable + get() = LocalReplacementShapes.current + } + // [END android_compose_designsystems_replace_material] + + // [START android_compose_designsystems_replace_button] + @Composable + fun ReplacementButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit + ) { + Button( + shape = ReplacementTheme.shapes.component, + onClick = onClick, + modifier = modifier, + content = { + ProvideTextStyle( + value = ReplacementTheme.typography.body + ) { + content() + } + } + ) + } + // [END android_compose_designsystems_replace_button] + + // [START android_compose_designsystems_replace_theme] + @Composable + fun ReplacementApp() { + ReplacementTheme { + /*...*/ + ReplacementButton(onClick = { /* ... */ }) { + /* ... */ + } + } + } + // [END android_compose_designsystems_replace_theme] +} + +object FullyCustomDesignSystem { + // [START android_compose_designsystems_fully_custom] + @Immutable + data class CustomColors( + val content: Color, + val component: Color, + val background: List + ) + + @Immutable + data class CustomTypography( + val body: TextStyle, + val title: TextStyle + ) + + @Immutable + data class CustomElevation( + val default: Dp, + val pressed: Dp + ) + + val LocalCustomColors = staticCompositionLocalOf { + CustomColors( + content = Color.Unspecified, + component = Color.Unspecified, + background = emptyList() + ) + } + val LocalCustomTypography = staticCompositionLocalOf { + CustomTypography( + body = TextStyle.Default, + title = TextStyle.Default + ) + } + val LocalCustomElevation = staticCompositionLocalOf { + CustomElevation( + default = Dp.Unspecified, + pressed = Dp.Unspecified + ) + } + + @Composable + fun CustomTheme( + /* ... */ + content: @Composable () -> Unit + ) { + val customColors = CustomColors( + content = Color(0xFFDD0D3C), + component = Color(0xFFC20029), + background = listOf(Color.White, Color(0xFFF8BBD0)) + ) + val customTypography = CustomTypography( + body = TextStyle(fontSize = 16.sp), + title = TextStyle(fontSize = 32.sp) + ) + val customElevation = CustomElevation( + default = 4.dp, + pressed = 8.dp + ) + CompositionLocalProvider( + LocalCustomColors provides customColors, + LocalCustomTypography provides customTypography, + LocalCustomElevation provides customElevation, + content = content + ) + } + + // Use with eg. CustomTheme.elevation.small + object CustomTheme { + val colors: CustomColors + @Composable + get() = LocalCustomColors.current + val typography: CustomTypography + @Composable + get() = LocalCustomTypography.current + val elevation: CustomElevation + @Composable + get() = LocalCustomElevation.current + } + // [END android_compose_designsystems_fully_custom] + + // [START android_compose_designsystems_fully_custom_usage] + @Composable + fun CustomButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit + ) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = CustomTheme.colors.component, + contentColor = CustomTheme.colors.content, + disabledContainerColor = CustomTheme.colors.content + .copy(alpha = 0.12f) + .compositeOver(CustomTheme.colors.component), + disabledContentColor = CustomTheme.colors.content + .copy(alpha = 0.38f) + + ), + shape = ButtonShape, + elevation = ButtonDefaults.elevatedButtonElevation( + defaultElevation = CustomTheme.elevation.default, + pressedElevation = CustomTheme.elevation.pressed + /* disabledElevation = 0.dp */ + ), + onClick = onClick, + modifier = modifier, + content = { + ProvideTextStyle( + value = CustomTheme.typography.body + ) { + content() + } + } + ) + } + + val ButtonShape = RoundedCornerShape(percent = 50) + // [END android_compose_designsystems_fully_custom_usage] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt new file mode 100644 index 000000000..e838a1b7f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material2Snippets.kt @@ -0,0 +1,411 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "DEPRECATION_ERROR", "UsingMaterialAndMaterial3Libraries") + +package com.example.compose.snippets.designsystems + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.shape.CutCornerShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.ContentAlpha +import androidx.compose.material.Icon +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalElevationOverlay +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Shapes +import androidx.compose.material.SnackbarDefaults.backgroundColor +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.Typography +import androidx.compose.material.contentColorFor +import androidx.compose.material.darkColors +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.lightColors +import androidx.compose.material.primarySurface +import androidx.compose.material.ripple.LocalRippleTheme +import androidx.compose.material.ripple.RippleTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.R + +@Composable +fun Material2() { + // [START android_compose_material2_theme] + MaterialTheme( + colors = // [START_EXCLUDE] + MaterialTheme.colors, + // [END_EXCLUDE] + typography = // [START_EXCLUDE] */ + MaterialTheme.typography, + // [END_EXCLUDE] + shapes = // [START_EXCLUDE] + MaterialTheme.shapes + // [END_EXCLUDE] + ) { + // app content + } + // [END android_compose_material2_theme] +} + +@Composable +fun Colors() { + // [START android_compose_material2_colors] + val Red = Color(0xffff0000) + val Blue = Color(red = 0f, green = 0f, blue = 1f) + // [END android_compose_material2_colors] +} + +private val Yellow400 = Color(0xffffeb46) +private val Yellow500 = Color(0xffffeb46) +private val Blue700 = Color(0xff91a4fc) + +// [START android_compose_material2_colors_builder] +private val Yellow200 = Color(0xffffeb46) +private val Blue200 = Color(0xff91a4fc) +// ... + +private val DarkColors = darkColors( + primary = Yellow200, + secondary = Blue200, + // ... +) +private val LightColors = lightColors( + primary = Yellow500, + primaryVariant = Yellow400, + secondary = Blue700, + // ... +) +// [END android_compose_material2_colors_builder] + +private val darkTheme = true + +@Composable +fun ThemeBuilder() { + // [START android_compose_material2_theme_builder] + MaterialTheme( + colors = if (darkTheme) DarkColors else LightColors + ) { + // app content + } + // [END android_compose_material2_theme_builder] +} + +@Composable +fun ColorUsage() { + // [START android_compose_material2_color_usage] + Text( + text = "Hello theming", + color = MaterialTheme.colors.primary + ) + // [END android_compose_material2_color_usage] + + val color = Color(0xffffeb46) + + // [START android_compose_material2_color_surface] + Surface( + color = MaterialTheme.colors.surface, + contentColor = contentColorFor(color), + // ... + ) { /* ... */ } + + TopAppBar( + backgroundColor = MaterialTheme.colors.primarySurface, + contentColor = contentColorFor(backgroundColor), + // ... + ) { /* ... */ } + // [END android_compose_material2_color_surface] + + // [START android_compose_material2_content_alpha] + // By default, both Icon & Text use the combination of LocalContentColor & + // LocalContentAlpha. De-emphasize content by setting content alpha + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Text( + // [START_EXCLUDE] + "" + // [END_EXCLUDE] + ) + } + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) { + Icon( + // [START_EXCLUDE] + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + // [END_EXCLUDE] + ) + Text( + // [START_EXCLUDE] + "" + // [END_EXCLUDE] + ) + } + // [END android_compose_material2_content_alpha] +} + +// [START android_compose_material2_dark_theme] +@Composable +fun MyTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + MaterialTheme( + colors = if (darkTheme) DarkColors else LightColors, + /*...*/ + content = content + ) +} +// [END android_compose_material2_dark_theme] + +@Composable +fun DarkTheme() { + // [START android_compose_material2_dark_theme_check] + val isLightTheme = MaterialTheme.colors.isLight + Icon( + painterResource( + id = if (isLightTheme) { + R.drawable.ic_sun_24 + } else { + R.drawable.ic_moon_24 + } + ), + contentDescription = "Theme" + ) + // [END android_compose_material2_dark_theme_check] +} + +@Composable +fun Elevation() { + // [START android_compose_material2_elevation] + Surface( + elevation = 2.dp, + color = MaterialTheme.colors.surface, // color will be adjusted for elevation + /*...*/ + ) { /*...*/ } + // [END android_compose_material2_elevation] + + // [START android_compose_material2_elevation_overlays] + // Elevation overlays + // Implemented in Surface (and any components that use it) + val color = MaterialTheme.colors.surface + val elevation = 4.dp + val overlaidColor = LocalElevationOverlay.current?.apply( + color, elevation + ) + // [END android_compose_material2_elevation_overlays] + + // [START android_compose_material2_elevation_overlays_disable] + MyTheme { + CompositionLocalProvider(LocalElevationOverlay provides null) { + // Content without elevation overlays + } + } + // [END android_compose_material2_elevation_overlays_disable] +} + +@Composable +fun ColorAccents() { + // [START android_compose_material2_color_accents] + Surface( + // Switches between primary in light theme and surface in dark theme + color = MaterialTheme.colors.primarySurface, + /*...*/ + ) { /*...*/ } + // [END android_compose_material2_color_accents] +} + +@Composable +fun TypeSystem() { + // [START android_compose_material2_typography] + val raleway = FontFamily( + Font(R.font.raleway_regular), + Font(R.font.raleway_medium, FontWeight.W500), + Font(R.font.raleway_semibold, FontWeight.SemiBold) + ) + + val myTypography = Typography( + h1 = TextStyle( + fontFamily = raleway, + fontWeight = FontWeight.W300, + fontSize = 96.sp + ), + body1 = TextStyle( + fontFamily = raleway, + fontWeight = FontWeight.W600, + fontSize = 16.sp + ) + /*...*/ + ) + MaterialTheme(typography = myTypography, /*...*/) { + /*...*/ + } + // [END android_compose_material2_typography] + + // [START android_compose_material2_typography_default] + val typography = Typography(defaultFontFamily = raleway) + MaterialTheme(typography = typography, /*...*/) { + /*...*/ + } + // [END android_compose_material2_typography_default] + + // [START android_compose_material2_typography_using] + Text( + text = "Subtitle2 styled", + style = MaterialTheme.typography.subtitle2 + ) + // [END android_compose_material2_typography_using] +} + +@Composable +fun ShapeSystem() { + // [START android_compose_material2_shapes] + val shapes = Shapes( + small = RoundedCornerShape(percent = 50), + medium = RoundedCornerShape(0f), + large = CutCornerShape( + topStart = 16.dp, + topEnd = 0.dp, + bottomEnd = 0.dp, + bottomStart = 16.dp + ) + ) + + MaterialTheme(shapes = shapes, /*...*/) { + /*...*/ + } + // [END android_compose_material2_shapes] + + // [START android_compose_material2_shapes_usage] + Surface( + shape = MaterialTheme.shapes.medium, /*...*/ + ) { + /*...*/ + } + // [END android_compose_material2_shapes_usage] +} + +// [START android_compose_material2_styles] +@Composable +fun MyButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit +) { + Button( + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.secondary + ), + onClick = onClick, + modifier = modifier, + content = content + ) +} +// [END android_compose_material2_styles] + +@Composable +fun PinkTheme( + content: @Composable () -> Unit +) { +} +@Composable +fun BlueTheme( + content: @Composable () -> Unit +) { +} + +// [START android_compose_material2_theme_overlays] +@Composable +fun DetailsScreen(/* ... */) { + PinkTheme { + // other content + RelatedSection() + } +} + +@Composable +fun RelatedSection(/* ... */) { + BlueTheme { + // content + } +} +// [END android_compose_material2_theme_overlays] + +@Composable +fun ComponentStates() { + // [START android_compose_material2_component_states] + Button( + onClick = { /* ... */ }, + enabled = true, + // Custom colors for different states + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.secondary, + disabledBackgroundColor = MaterialTheme.colors.onBackground + .copy(alpha = 0.2f) + .compositeOver(MaterialTheme.colors.background) + // Also contentColor and disabledContentColor + ), + // Custom elevation for different states + elevation = ButtonDefaults.elevation( + defaultElevation = 8.dp, + disabledElevation = 2.dp, + // Also pressedElevation + ) + ) { /* ... */ } + // [END android_compose_material2_component_states] +} + +// [START android_compose_material2_ripples] +@Composable +fun MyApp() { + MaterialTheme { + CompositionLocalProvider( + LocalRippleTheme provides SecondaryRippleTheme + ) { + // App content + } + } +} + +@Immutable +private object SecondaryRippleTheme : RippleTheme { + @Composable + override fun defaultColor() = RippleTheme.defaultRippleColor( + contentColor = MaterialTheme.colors.secondary, + lightTheme = MaterialTheme.colors.isLight + ) + + @Composable + override fun rippleAlpha() = RippleTheme.defaultRippleAlpha( + contentColor = MaterialTheme.colors.secondary, + lightTheme = MaterialTheme.colors.isLight + ) +} +// [END android_compose_material2_ripples] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material3Snippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material3Snippets.kt new file mode 100644 index 000000000..5d5d72d37 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/Material3Snippets.kt @@ -0,0 +1,475 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.designsystems + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.PermanentNavigationDrawer +import androidx.compose.material3.Shapes +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.BaselineShift +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.R +import com.example.compose.snippets.ui.theme.Typography + +private object Material3Snippets { + // [START android_compose_material3_experimental_annotation] + // import androidx.compose.material3.ExperimentalMaterial3Api + @Composable + fun AppComposable() { + // M3 composables + } + + // [END android_compose_material3_experimental_annotation] +} + +@Composable +private fun Material3ThemeStart() { + // [START android_compose_material3_theme_definition] + MaterialTheme( + colorScheme = /* [START_EXCLUDE] */lightColorScheme()/* [END_EXCLUDE] */, + typography = /* [START_EXCLUDE] */Typography/* [END_EXCLUDE] */, + shapes = /* [START_EXCLUDE] */Shapes()/* [END_EXCLUDE] */ + ) { + // M3 app content + } + // [END android_compose_material3_theme_definition] +} + +private object ColorScheme { + // [START android_compose_material3_theme_colors] + val md_theme_light_primary = Color(0xFF476810) + val md_theme_light_onPrimary = Color(0xFFFFFFFF) + val md_theme_light_primaryContainer = Color(0xFFC7F089) + // .. + // .. + + val md_theme_dark_primary = Color(0xFFACD370) + val md_theme_dark_onPrimary = Color(0xFF213600) + val md_theme_dark_primaryContainer = Color(0xFF324F00) + // .. + // .. + // [END android_compose_material3_theme_colors] + + // [START android_compose_material3_theme_color_setup] + private val LightColorScheme = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + // .. + ) + private val DarkColorScheme = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + // .. + ) + + @Composable + fun ReplyTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit + ) { + val colorScheme = + if (!darkTheme) { + LightColorScheme + } else { + DarkColorScheme + } + MaterialTheme( + colorScheme = colorScheme, + content = content + ) + } + // [END android_compose_material3_theme_color_setup] + + @Composable + private fun DynamicThemeSetup(darkTheme: Boolean = isSystemInDarkTheme()) { + // [START android_compose_material3_theme_dynamic_color] + // Dynamic color is available on Android 12+ + val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + val colors = when { + dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current) + dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current) + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + // [END android_compose_material3_theme_dynamic_color] + } + + @Composable + private fun UsingColorTheme() { + // [START android_compose_material3_use_color_theme] + Text( + text = "Hello theming", + color = MaterialTheme.colorScheme.primary + ) + // [END android_compose_material3_use_color_theme] + } + + @Composable + private fun UseColorTheme2() { + val isSelected by remember { + mutableStateOf(false) + } + // [START android_compose_material3_use_color_theme_2] + Card( + colors = CardDefaults.cardColors( + containerColor = + if (isSelected) MaterialTheme.colorScheme.primaryContainer + else + MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Text( + text = "Dinner club", + style = MaterialTheme.typography.bodyLarge, + color = + if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onSurface, + ) + } + + // [END android_compose_material3_use_color_theme_2] + } +} + +private object TypographySnippets { + // [START android_compose_material3_typography_definition] + val replyTypography = Typography( + titleLarge = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + titleMedium = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + // .. + ) + // .. + // [END android_compose_material3_typography_definition] + + val replyTypography2 = Typography( + // [START android_compose_material3_typography_body_large] + bodyLarge = TextStyle( + fontWeight = FontWeight.Normal, + fontFamily = FontFamily.SansSerif, + fontStyle = FontStyle.Italic, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + baselineShift = BaselineShift.Subscript + ), + // [END android_compose_material3_typography_body_large] + ) + + @Composable + private fun TypographyThemeSetup() { + // [START android_compose_material3_typography_theme_setup] + MaterialTheme( + typography = replyTypography, + ) { + // M3 app Content + } + // [END android_compose_material3_typography_theme_setup] + } + + @Composable + private fun TypographyUse() { + // [START android_compose_material3_typography_use] + Text( + text = "Hello M3 theming", + style = MaterialTheme.typography.titleLarge + ) + Text( + text = "you are learning typography", + style = MaterialTheme.typography.bodyMedium + ) + // [END android_compose_material3_typography_use] + } +} + +private object Material3ShapesSetup { + // [START android_compose_material3_shape_setup] + val replyShapes = Shapes( + extraSmall = RoundedCornerShape(4.dp), + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(12.dp), + large = RoundedCornerShape(16.dp), + extraLarge = RoundedCornerShape(24.dp) + ) + // [END android_compose_material3_shape_setup] + + @Composable + private fun ShapeThemeSetup() { + // [START android_compose_material3_shape_theme] + MaterialTheme( + shapes = replyShapes, + ) { + // M3 app Content + } + // [END android_compose_material3_shape_theme] + } + + @Composable + private fun ShapeUsage() { + // [START android_compose_material3_shape_usage] + Card(shape = MaterialTheme.shapes.medium) { /* card content */ } + FloatingActionButton( + shape = MaterialTheme.shapes.large, + onClick = { + } + ) { + /* fab content */ + } + // [END android_compose_material3_shape_usage] + } + @Composable + private fun ShapeUsage2() { + // [START android_compose_material3_shape_usage_2] + Card(shape = RectangleShape) { /* card content */ } + Card(shape = CircleShape) { /* card content */ } + // [END android_compose_material3_shape_usage_2] + } +} + +private object EmphasisSnippets { + val replyTypography = Typography( + // [START android_compose_material3_emphasis] + bodyLarge = TextStyle( + fontWeight = FontWeight.Bold + ), + bodyMedium = TextStyle( + fontWeight = FontWeight.Normal + ) + // [END android_compose_material3_emphasis] + ) +} +@Composable +private fun ElevationSnippets(content: @Composable ColumnScope.() -> Unit) { + // [START android_compose_material3_elevation] + Surface( + modifier = Modifier, + tonalElevation = /*[START_EXCLUDE] */ 2.dp/*[END_EXCLUDE]*/, + shadowElevation = /*[START_EXCLUDE] */ 2.dp/*[END_EXCLUDE]*/ + ) { + Column(content = content) + } + // [END android_compose_material3_elevation] +} + +private object MaterialComponentsExamples { + @Composable + fun ButtonUsage() { + // [START android_compose_material3_button_usage] + Button(onClick = { /*..*/ }) { + Text(text = "My Button") + } + // [END android_compose_material3_button_usage] + } + + @Composable + fun ExtendedFloatingActionButtonUsage() { + // [START android_compose_material3_extended_button_usage] + ExtendedFloatingActionButton( + onClick = { /*..*/ }, + modifier = Modifier + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(id = R.string.edit), + ) + Text( + text = stringResource(id = R.string.add_entry), + ) + } + // [END android_compose_material3_extended_button_usage] + } + @Composable + fun FilledButtonUsageHighEmphasis() { + // [START android_compose_material3_filled_button_high_emphasis] + Button(onClick = { /*..*/ }) { + Text(text = stringResource(id = R.string.view_entry)) + } + // [END android_compose_material3_filled_button_high_emphasis] + } + @Composable + fun TextButtonUsageLowEmphasis() { + // [START android_compose_material3_text_button_low_emphasis] + TextButton(onClick = { /*..*/ }) { + Text(text = stringResource(id = R.string.replated_articles)) + } + // [END android_compose_material3_text_button_low_emphasis] + } + enum class Destinations { + Home, + Emails, + Sent, + Drafts + } + @Composable + fun NavBarUsage() { + val selectedDestination by remember { + mutableStateOf(Destinations.Home) + } + // [START android_compose_material3_nav_bar_usage] + NavigationBar(modifier = Modifier.fillMaxWidth()) { + Destinations.entries.forEach { replyDestination -> + NavigationBarItem( + selected = selectedDestination == replyDestination, + onClick = { }, + icon = { } + ) + } + } + // [END android_compose_material3_nav_bar_usage] + } + @Composable + fun NavRailUsage() { + val selectedDestination by remember { + mutableStateOf(Destinations.Home) + } + // [START android_compose_material3_nav_rail_usage] + NavigationRail( + modifier = Modifier.fillMaxHeight(), + ) { + Destinations.entries.forEach { replyDestination -> + NavigationRailItem( + selected = selectedDestination == replyDestination, + onClick = { }, + icon = { } + ) + } + } + // [END android_compose_material3_nav_rail_usage] + } + @Composable + fun PermanentNavDrawerUsage() { + val selectedDestination by remember { + mutableStateOf(Destinations.Home) + } + // [START android_compose_material3_permanent_nav_drawer] + PermanentNavigationDrawer(modifier = Modifier.fillMaxHeight(), drawerContent = { + Destinations.entries.forEach { replyDestination -> + NavigationRailItem( + selected = selectedDestination == replyDestination, + onClick = { }, + icon = { }, + label = { } + ) + } + }) { + } + // [END android_compose_material3_permanent_nav_drawer] + } + + @Composable + fun CardThemingExample() { + // [START android_compose_material3_card_theming] + val customCardColors = CardDefaults.cardColors( + contentColor = MaterialTheme.colorScheme.primary, + containerColor = MaterialTheme.colorScheme.primaryContainer, + disabledContentColor = MaterialTheme.colorScheme.surface, + disabledContainerColor = MaterialTheme.colorScheme.onSurface, + ) + val customCardElevation = CardDefaults.cardElevation( + defaultElevation = 8.dp, + pressedElevation = 2.dp, + focusedElevation = 4.dp + ) + Card( + colors = customCardColors, + elevation = customCardElevation + ) { + // m3 card content + } + // [END android_compose_material3_card_theming] + } +} + +private object MaterialAccessibilityExamples { + @Composable + fun ButtonContrastExample() { + // [START android_compose_material3_button_contrast_example] + // ✅ Button with sufficient contrast ratio + Button( + onClick = { }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + } + + // ❌ Button with poor contrast ratio + Button( + onClick = { }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + } + // [END android_compose_material3_button_contrast_example] + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/ThemeAnatomySnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/ThemeAnatomySnippets.kt new file mode 100644 index 000000000..b108ce6b6 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/designsystems/ThemeAnatomySnippets.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.designsystems + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.sp + +// [START android_compose_anatomy_classes] +@Immutable +data class ColorSystem( + val color: Color, + val gradient: List + /* ... */ +) + +@Immutable +data class TypographySystem( + val fontFamily: FontFamily, + val textStyle: TextStyle +) +/* ... */ + +@Immutable +data class CustomSystem( + val value1: Int, + val value2: String + /* ... */ +) + +/* ... */ +// [END android_compose_anatomy_classes] + +// [START android_compose_anatomy_composition_locals] +val LocalColorSystem = staticCompositionLocalOf { + ColorSystem( + color = Color.Unspecified, + gradient = emptyList() + ) +} + +val LocalTypographySystem = staticCompositionLocalOf { + TypographySystem( + fontFamily = FontFamily.Default, + textStyle = TextStyle.Default + ) +} + +val LocalCustomSystem = staticCompositionLocalOf { + CustomSystem( + value1 = 0, + value2 = "" + ) +} + +/* ... */ +// [END android_compose_anatomy_composition_locals] + +// [START android_compose_anatomy_functions] +@Composable +fun Theme( + /* ... */ + content: @Composable () -> Unit +) { + val colorSystem = ColorSystem( + color = Color(0xFF3DDC84), + gradient = listOf(Color.White, Color(0xFFD7EFFF)) + ) + val typographySystem = TypographySystem( + fontFamily = FontFamily.Monospace, + textStyle = TextStyle(fontSize = 18.sp) + ) + val customSystem = CustomSystem( + value1 = 1000, + value2 = "Custom system" + ) + /* ... */ + CompositionLocalProvider( + LocalColorSystem provides colorSystem, + LocalTypographySystem provides typographySystem, + LocalCustomSystem provides customSystem, + /* ... */ + content = content + ) +} +// [END android_compose_anatomy_functions] + +// [START android_compose_anatomy_object] +// Use with eg. Theme.colorSystem.color +object Theme { + val colorSystem: ColorSystem + @Composable + get() = LocalColorSystem.current + val typographySystem: TypographySystem + @Composable + get() = LocalTypographySystem.current + val customSystem: CustomSystem + @Composable + get() = LocalCustomSystem.current + /* ... */ +} +// [END android_compose_anatomy_object] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt new file mode 100644 index 000000000..12542079e --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/draganddrop/DragAndDropSnippets.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.draganddrop + +import android.content.ClipData +import android.content.ClipDescription +import android.os.Build +import android.view.View +import androidx.activity.compose.LocalActivity +import androidx.annotation.RequiresApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.draganddrop.dragAndDropSource +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.DragAndDropTransferData +import androidx.compose.ui.draganddrop.mimeTypes +import androidx.compose.ui.draganddrop.toAndroidDragEvent + +@RequiresApi(Build.VERSION_CODES.N) +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun DragAndDropSnippet() { + + val url = "" + + // [START android_compose_drag_and_drop_2] + Modifier.dragAndDropSource { _ -> + DragAndDropTransferData( + ClipData.newPlainText( + "image Url", url + ) + ) + } + // [END android_compose_drag_and_drop_2] + + // [START android_compose_drag_and_drop_3] + Modifier.dragAndDropSource { _ -> + DragAndDropTransferData( + ClipData.newPlainText( + "image Url", url + ), + flags = View.DRAG_FLAG_GLOBAL + ) + } + // [END android_compose_drag_and_drop_3] + + // [START android_compose_drag_and_drop_4] + val callback = remember { + object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + // Parse received data + return true + } + } + } + // [END android_compose_drag_and_drop_4] + + LocalActivity.current?.let { activity -> + // [START android_compose_drag_and_drop_7] + val externalAppCallback = remember { + object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + val permission = + activity.requestDragAndDropPermissions(event.toAndroidDragEvent()) + // Parse received data + permission?.release() + return true + } + } + } + // [END android_compose_drag_and_drop_7] + } + + // [START android_compose_drag_and_drop_5] + Modifier.dragAndDropTarget( + shouldStartDragAndDrop = { event -> + event.mimeTypes().contains(ClipDescription.MIMETYPE_TEXT_PLAIN) + }, target = callback // or externalAppCallback + ) + // [END android_compose_drag_and_drop_5] + + // [START android_compose_drag_and_drop_6] + object : DragAndDropTarget { + override fun onStarted(event: DragAndDropEvent) { + // When the drag event starts + } + + override fun onEntered(event: DragAndDropEvent) { + // When the dragged object enters the target surface + } + + override fun onEnded(event: DragAndDropEvent) { + // When the drag event stops + } + + override fun onExited(event: DragAndDropEvent) { + // When the dragged object exits the target surface + } + + override fun onDrop(event: DragAndDropEvent): Boolean = true + } + // [END android_compose_drag_and_drop_6] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceMetrics.kt b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceMetrics.kt new file mode 100644 index 000000000..3541cceb4 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceMetrics.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.glance + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi + +private const val TAG = "WidgetMetrics" + +class GlanceMetrics { + + // [START android_compose_glance_metrics] + @RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1) + fun getWidgetEngagementMetrics(context: Context) { + val manager = AppWidgetManager.getInstance(context) + + val endTime = System.currentTimeMillis() + val startTime = endTime - (24 * 60 * 60 * 1000) // a day ago + + val events = manager.queryAppWidgetEvents(startTime, endTime) + + if (events.isEmpty()) { + Log.d(TAG, "No events found for the given time range.") + } + + val metrics = hashMapOf( + "clicks" to 0L, + "scrolls" to 0L, + "totalImpressionLength" to 0L + ) + + for (event in events) { + + Log.d(TAG, "Event Start: ${event.start}") + Log.d(TAG, "Event End: ${event.end}") + + val widgetId = event.appWidgetId + + // Tap actions + val clickedIds = event.clickedIds + if (clickedIds?.isNotEmpty() == true) { + metrics["clicks"] = metrics.getValue("clicks") + clickedIds.size + // Log or analyze which components were clicked. + for (id in clickedIds) { + Log.d(TAG, "Widget $widgetId: Tap event on component with ID $id") + } + } + + // Scroll events + val scrolledIds = event.scrolledIds + if (scrolledIds?.isNotEmpty() == true) { + metrics["scrolls"] = metrics.getValue("scrolls") + scrolledIds.size + // Log or analyze which lists were scrolled. + for (id in scrolledIds) { + Log.d(TAG, "Widget $widgetId: Scroll event in list with ID/tag $id") + } + } + + // Impressions + metrics["totalImpressionLength"] = metrics.getValue("totalImpressionLength") + event.visibleDuration.toMillis() + Log.d( + TAG, + "Widget $widgetId: Impression event with duration " + event.visibleDuration.toMillis() + "ms" + ) + + // Position + val position = event.position + if (position != null) { + Log.d( + TAG, + "Widget $widgetId: left=${position.left}, right=${position.right}, top=${position.top}, bottom=${position.bottom}" + ) + } + } + Log.d("WidgetMetrics", "Metrics: $metrics") + } + // [END android_compose_glance_metrics] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlancePinAppWidget.kt b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlancePinAppWidget.kt new file mode 100644 index 000000000..428212068 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlancePinAppWidget.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.glance + +import android.content.Context +import androidx.compose.material3.Button +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import kotlinx.coroutines.launch + +class MyWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = MyWidget() +} + +class MyWidget : GlanceAppWidget() { + override suspend fun provideGlance( + context: Context, + id: GlanceId + ) {} +} + +// [START android_compose_glance_in_app_pinning] +@Composable +fun AnInAppComposable() { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + Button( + onClick = { + coroutineScope.launch { + GlanceAppWidgetManager(context).requestPinGlanceAppWidget( + receiver = MyWidgetReceiver::class.java, + preview = MyWidget(), + previewState = DpSize(245.dp, 115.dp) + ) + } + } + ) {} +} +// [END android_compose_glance_in_app_pinning] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt new file mode 100644 index 000000000..df5efbd79 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/glance/GlanceSnippets.kt @@ -0,0 +1,1023 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.glance + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.widget.RemoteViews +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.glance.Button +import androidx.glance.ColorFilter +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.LocalSize +import androidx.glance.action.ActionParameters +import androidx.glance.action.actionParametersOf +import androidx.glance.action.actionStartActivity +import androidx.glance.action.clickable +import androidx.glance.appwidget.AndroidRemoteViews +import androidx.glance.appwidget.CheckBox +import androidx.glance.appwidget.CheckboxDefaults +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import androidx.glance.appwidget.RadioButton +import androidx.glance.appwidget.RadioButtonDefaults +import androidx.glance.appwidget.SizeMode +import androidx.glance.appwidget.Switch +import androidx.glance.appwidget.SwitchDefaults +import androidx.glance.appwidget.action.ActionCallback +import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.action.actionSendBroadcast +import androidx.glance.appwidget.action.actionStartService +import androidx.glance.appwidget.cornerRadius +import androidx.glance.appwidget.provideContent +import androidx.glance.appwidget.updateAll +import androidx.glance.appwidget.updateIf +import androidx.glance.background +import androidx.glance.color.ColorProvider +import androidx.glance.layout.Alignment +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.RowScope +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.padding +import androidx.glance.material3.ColorProviders +import androidx.glance.text.Text +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.example.compose.snippets.MyActivity +import com.example.compose.snippets.R +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +lateinit var LightColors: ColorScheme +lateinit var DarkColors: ColorScheme + +private object GlanceCreateAppWidgetSnippet01 { + // [START android_compose_glance_receiver01] + class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = TODO("Create GlanceAppWidget") + } + // [END android_compose_glance_receiver01] +} + +private object GlanceCreateAppWidgetSnippet02 { + // [START android_compose_glance_receiver02] + class MyAppWidgetReceiver : GlanceAppWidgetReceiver() { + + // Let MyAppWidgetReceiver know which GlanceAppWidget to use + override val glanceAppWidget: GlanceAppWidget = MyAppWidget() + } + // [END android_compose_glance_receiver02] + + // [START android_compose_glance_widget] + class MyAppWidget : GlanceAppWidget() { + + override suspend fun provideGlance(context: Context, id: GlanceId) { + + // In this method, load data needed to render the AppWidget. + // Use `withContext` to switch to another thread for long running + // operations. + + provideContent { + // create your AppWidget here + Text("Hello World") + } + } + } + // [END android_compose_glance_widget] +} + +private object CreateUI { + // [START android_compose_glance_createui] + /* Import Glance Composables + In the event there is a name clash with the Compose classes of the same name, + you may rename the imports per https://kotlinlang.org/docs/packages.html#imports + using the `as` keyword. + + import androidx.glance.Button + import androidx.glance.layout.Column + import androidx.glance.layout.Row + import androidx.glance.text.Text + */ + class MyAppWidget : GlanceAppWidget() { + + override suspend fun provideGlance(context: Context, id: GlanceId) { + // Load data needed to render the AppWidget. + // Use `withContext` to switch to another thread for long running + // operations. + + provideContent { + // create your AppWidget here + MyContent() + } + } + + @Composable + private fun MyContent() { + Column( + modifier = GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp)) + Row(horizontalAlignment = Alignment.CenterHorizontally) { + Button( + text = "Home", + onClick = actionStartActivity() + ) + Button( + text = "Work", + onClick = actionStartActivity() + ) + } + } + } + } + // [END android_compose_glance_createui] +} + +private object ActionLaunchActivity { + + // [START android_compose_glance_launchactivity] + @Composable + fun MyContent() { + // .. + Button( + text = "Go Home", + onClick = actionStartActivity() + ) + } + // [END android_compose_glance_launchactivity] +} + +private object ActionLaunchService { + + // [START android_compose_glance_launchservice] + @Composable + fun MyButton() { + // .. + Button( + text = "Sync", + onClick = actionStartService( + isForegroundService = true // define how the service is launched + ) + ) + } + // [END android_compose_glance_launchservice] +} + +private object ActionLaunchSendBroadcastEvent { + + // [START android_compose_glance_sendbroadcastevent] + @Composable + fun MyButton() { + // .. + Button( + text = "Send", + onClick = actionSendBroadcast() + ) + } + // [END android_compose_glance_sendbroadcastevent] +} + +private object ActionLambda { + @Composable + fun ActionLambda() { + // [START android_compose_glance_lambda01] + Text( + text = "Submit", + modifier = GlanceModifier.clickable { + submitData() + } + ) + // [END android_compose_glance_lambda01] + } + + @Composable + fun ActionLambda2() { + // [START android_compose_glance_lambda02] + Button( + text = "Submit", + onClick = { + submitData() + } + ) + // [END android_compose_glance_lambda02] + } +} + +private object ActionCallbackSnippet02 { + // [START android_compose_glance_actioncallback02] + class RefreshAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + // do some work but offset long-term tasks (e.g a Worker) + MyAppWidget().update(context, glanceId) + } + } + + // [END android_compose_glance_actioncallback02] + /*dummy class*/ + class MyAppWidget : GlanceAppWidget() { + override suspend fun provideGlance(context: Context, id: GlanceId) { + TODO("Not yet implemented") + } + } +} + +private object ActionCallbackSnippet01 { + // [START android_compose_glance_actioncallback01] + @Composable + private fun MyContent() { + // .. + Image( + provider = ImageProvider(R.drawable.ic_hourglass_animated), + modifier = GlanceModifier.clickable( + onClick = actionRunCallback() + ), + contentDescription = "Refresh" + ) + } + + class RefreshAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + // TODO implement + } + } + // [END android_compose_glance_actioncallback01] +} + +private object ActionParameters { + // [START android_compose_glance_actionparameters01] + private val destinationKey = ActionParameters.Key( + NavigationActivity.KEY_DESTINATION + ) + + class MyAppWidget : GlanceAppWidget() { + + // .. + + @Composable + private fun MyContent() { + // .. + Button( + text = "Home", + onClick = actionStartActivity( + actionParametersOf(destinationKey to "home") + ) + ) + Button( + text = "Work", + onClick = actionStartActivity( + actionParametersOf(destinationKey to "work") + ) + ) + } + + override suspend fun provideGlance(context: Context, id: GlanceId) { + provideContent { MyContent() } + } + } + // [END android_compose_glance_actionparameters01] + + abstract class ActionParametersActivity : Activity() { + val KEY_DESTINATION = "destination" + + // [START android_compose_glance_actionparameters02] + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val destination = intent.extras?.getString(KEY_DESTINATION) ?: return + // ... + } + } + // [END android_compose_glance_actionparameters02] + + // [START android_compose_glance_actionparameters03] + class RefreshAction : ActionCallback { + + private val destinationKey = ActionParameters.Key( + NavigationActivity.KEY_DESTINATION + ) + + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + val destination: String = parameters[destinationKey] ?: return + // ... + } + } + // [END android_compose_glance_actionparameters03] +} + +@SuppressLint("RememberReturnType") +object ManageAndUpdate { + abstract + // [START android_compose_glance_manageupdate01] + class DestinationAppWidget : GlanceAppWidget() { + + // ... + + @Composable + fun MyContent() { + val repository = remember { DestinationsRepository.getInstance() } + // Retrieve the cache data everytime the content is refreshed + val destinations by repository.destinations.collectAsState(State.Loading) + + when (destinations) { + is State.Loading -> { + // show loading content + } + + is State.Error -> { + // show widget error content + } + + is State.Completed -> { + // show the list of destinations + } + } + } + } + // [END android_compose_glance_manageupdate01] + + suspend fun update02(context: Context, glanceId: GlanceId) { + // [START android_compose_glance_manageupdate02] + MyAppWidget().update(context, glanceId) + // [END android_compose_glance_manageupdate02] + + // [START android_compose_glance_manageupdate03] + val manager = GlanceAppWidgetManager(context) + val widget = GlanceSizeModeWidget() + val glanceIds = manager.getGlanceIds(widget.javaClass) + glanceIds.forEach { glanceId -> + widget.update(context, glanceId) + } + // [END android_compose_glance_manageupdate03] + + // [START android_compose_glance_manageupdate04] + // Updates all placed instances of MyAppWidget + MyAppWidget().updateAll(context) + + // Iterate over all placed instances of MyAppWidget and update if the state of + // the instance matches the given predicate + MyAppWidget().updateIf(context) { state -> + state == State.Completed + } + // [END android_compose_glance_manageupdate04] + } + + // [START android_compose_glance_manageupdate05] + class DataSyncWorker( + val context: Context, + val params: WorkerParameters, + ) : CoroutineWorker(context, params) { + + override suspend fun doWork(): Result { + // Fetch data or do some work and then update all instance of your widget + MyAppWidget().updateAll(context) + return Result.success() + } + } +// [END android_compose_glance_manageupdate05] +} + +object BuildUIWithGlance { + + @Composable + fun Example1() { + // [START android_compose_glance_buildUI01] + Row(modifier = GlanceModifier.fillMaxWidth().padding(16.dp)) { + val modifier = GlanceModifier.defaultWeight() + Text("first", modifier) + Text("second", modifier) + Text("third", modifier) + } + // [END android_compose_glance_buildUI01] + } + + @Composable + fun Example2() { + + // [START android_compose_glance_buildUI02] + // Remember to import Glance Composables + // import androidx.glance.appwidget.layout.LazyColumn + + LazyColumn { + items(10) { index: Int -> + Text( + text = "Item $index", + modifier = GlanceModifier.fillMaxWidth() + ) + } + } + // [END android_compose_glance_buildUI02] + } + + @Composable + fun Example3() { + // [START android_compose_glance_buildUI03] + LazyColumn { + item { + Text("First Item") + } + item { + Text("Second Item") + } + } + // [END android_compose_glance_buildUI03] + } + + @Composable + fun Example4() { + val peopleNameList = arrayListOf() + val peopleList = arrayListOf() + + // [START android_compose_glance_buildUI04] + LazyColumn { + items(peopleNameList) { name -> + Text(name) + } + } + // [END android_compose_glance_buildUI04] + + // [START android_compose_glance_buildUI05] + LazyColumn { + item { + Text("Names:") + } + items(peopleNameList) { name -> + Text(name) + } + + // or in case you need the index: + itemsIndexed(peopleNameList) { index, person -> + Text("$person at index $index") + } + } + // [END android_compose_glance_buildUI05] + + LazyColumn { + // [START android_compose_glance_buildUI06] + items(items = peopleList, key = { person -> person.id }) { person -> + Text(person.name) + } + // [END android_compose_glance_buildUI06] + } + } +} + +object SizeModeSnippets { + + // [START android_compose_glance_buildUI07] + class MyAppWidget : GlanceAppWidget() { + + override val sizeMode = SizeMode.Single + + override suspend fun provideGlance(context: Context, id: GlanceId) { + // ... + + provideContent { + MyContent() + } + } + + @Composable + private fun MyContent() { + // Size will be the minimum size or resizable + // size defined in the App Widget metadata + val size = LocalSize.current + // ... + } + } + // [END android_compose_glance_buildUI07] +} + +object SizeModeSnippets2 { + // [START android_compose_glance_buildUI08] + class MyAppWidget : GlanceAppWidget() { + + companion object { + private val SMALL_SQUARE = DpSize(100.dp, 100.dp) + private val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp) + private val BIG_SQUARE = DpSize(250.dp, 250.dp) + } + + override val sizeMode = SizeMode.Responsive( + setOf( + SMALL_SQUARE, + HORIZONTAL_RECTANGLE, + BIG_SQUARE + ) + ) + + override suspend fun provideGlance(context: Context, id: GlanceId) { + // ... + + provideContent { + MyContent() + } + } + + @Composable + private fun MyContent() { + // Size will be one of the sizes defined above. + val size = LocalSize.current + Column { + if (size.height >= BIG_SQUARE.height) { + Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp)) + } + Row(horizontalAlignment = Alignment.CenterHorizontally) { + Button() + Button() + if (size.width >= HORIZONTAL_RECTANGLE.width) { + Button("School") + } + } + if (size.height >= BIG_SQUARE.height) { + Text(text = "provided by X") + } + } + } + } + + // [END android_compose_glance_buildUI08] +} + +object SizeModeSnippets3 { + // [START android_compose_glance_buildUI09] + + class MyAppWidget : GlanceAppWidget() { + + override val sizeMode = SizeMode.Exact + + override suspend fun provideGlance(context: Context, id: GlanceId) { + // ... + + provideContent { + MyContent() + } + } + + @Composable + private fun MyContent() { + // Size will be the size of the AppWidget + val size = LocalSize.current + Column { + Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp)) + Row(horizontalAlignment = Alignment.CenterHorizontally) { + Button() + Button() + if (size.width > 250.dp) { + Button("School") + } + } + } + } + } + // [END android_compose_glance_buildUI09] +} + +object AccessResources { + @Composable + fun Example1() { + // [START android_compose_glance_buildUI10] + LocalContext.current.getString(R.string.glance_title) + // [END android_compose_glance_buildUI10] + + // [START android_compose_glance_buildUI11] + Column( + modifier = GlanceModifier.background(R.color.default_widget_background) + ) { /**...*/ } + + Image( + provider = ImageProvider(R.drawable.ic_logo), + contentDescription = "My image", + ) + // [END android_compose_glance_buildUI11] + } +} + +object CompoundButton { + @Composable + fun Example1() { + // [START android_compose_glance_buildUI12] + var isApplesChecked by remember { mutableStateOf(false) } + var isEnabledSwitched by remember { mutableStateOf(false) } + var isRadioChecked by remember { mutableIntStateOf(0) } + + CheckBox( + checked = isApplesChecked, + onCheckedChange = { isApplesChecked = !isApplesChecked }, + text = "Apples" + ) + + Switch( + checked = isEnabledSwitched, + onCheckedChange = { isEnabledSwitched = !isEnabledSwitched }, + text = "Enabled" + ) + + RadioButton( + checked = isRadioChecked == 1, + onClick = { isRadioChecked = 1 }, + text = "Checked" + ) + // [END android_compose_glance_buildUI12] + } + // [START android_compose_glance_buildUI13] + class MyAppWidget : GlanceAppWidget() { + + override suspend fun provideGlance(context: Context, id: GlanceId) { + val myRepository = MyRepository.getInstance() + + provideContent { + val scope = rememberCoroutineScope() + + val saveApple: (Boolean) -> Unit = + { scope.launch { myRepository.saveApple(it) } } + MyContent(saveApple) + } + } + + @Composable + private fun MyContent(saveApple: (Boolean) -> Unit) { + + var isAppleChecked by remember { mutableStateOf(false) } + + Button( + text = "Save", + onClick = { saveApple(isAppleChecked) } + ) + } + } + // [END android_compose_glance_buildUI13] + + @Composable + fun example3() { + val colorAccentDay = Color.Blue + val colorAccentNight = Color.Blue + var isChecked by remember { mutableStateOf(false) } + + // [START android_compose_glance_buildUI14] + CheckBox( + // ... + colors = CheckboxDefaults.colors( + checkedColor = ColorProvider(day = colorAccentDay, night = colorAccentNight), + uncheckedColor = ColorProvider(day = Color.DarkGray, night = Color.LightGray) + ), + checked = isChecked, + onCheckedChange = { isChecked = !isChecked } + ) + + Switch( + // ... + colors = SwitchDefaults.colors( + checkedThumbColor = ColorProvider(day = Color.Red, night = Color.Cyan), + uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Magenta), + checkedTrackColor = ColorProvider(day = Color.Blue, night = Color.Yellow), + uncheckedTrackColor = ColorProvider(day = Color.Magenta, night = Color.Green) + ), + checked = isChecked, + onCheckedChange = { isChecked = !isChecked }, + text = "Enabled" + ) + + RadioButton( + // ... + colors = RadioButtonDefaults.colors( + checkedColor = ColorProvider(day = Color.Cyan, night = Color.Yellow), + uncheckedColor = ColorProvider(day = Color.Red, night = Color.Blue) + ), + + ) + // [END android_compose_glance_buildUI14] + } + + private fun RadioButton(colors: Any) { + } +} + +object GlanceTheming { + + class ExampleAppWidget : GlanceAppWidget() { + // [START android_compose_glance_glancetheming01] + + override suspend fun provideGlance(context: Context, id: GlanceId) { + + provideContent { + GlanceTheme { + MyContent() + } + } + } + + @Composable + private fun MyContent() { + + Image( + colorFilter = ColorFilter.tint(GlanceTheme.colors.secondary), + // [START_EXCLUDE] + contentDescription = "Example Image", + provider = ImageProvider(R.drawable.ic_logo) + // [END_EXCLUDE] + + ) + } + // [END android_compose_glance_glancetheming01] + } + + class ExampleAppWidget2 : GlanceAppWidget() { + + // [START android_compose_glance_glancetheming06] + // Remember, use the Glance imports + // import androidx.glance.material3.ColorProviders + + // Example Imports from your own app + // import com.example.myapp.ui.theme.DarkColors + // import com.example.myapp.ui.theme.LightColors + + object MyAppWidgetGlanceColorScheme { + + val colors = ColorProviders( + light = LightColors, + dark = DarkColors + ) + } + // [END android_compose_glance_glancetheming06] + // [START android_compose_glance_glancetheming02] + override suspend fun provideGlance(context: Context, id: GlanceId) { + // ... + + provideContent { + GlanceTheme(colors = MyAppWidgetGlanceColorScheme.colors) { + MyContent() + } + } + } + + @Composable + private fun MyContent() { + + Image( + colorFilter = ColorFilter.tint(GlanceTheme.colors.secondary), + // [START_EXCLUDE] + provider = ImageProvider(R.drawable.ic_logo), + contentDescription = "Example" + // [END_EXCLUDE] + ) + } + // [END android_compose_glance_glancetheming02] + } + + class ExampleAppWidget3 : GlanceAppWidget() { + + object MyAppWidgetGlanceColorScheme { + val colors = ColorProviders( + light = LightColors, + dark = DarkColors + ) + } + + // [START android_compose_glance_glancetheming03] + override suspend fun provideGlance(context: Context, id: GlanceId) { + + provideContent { + GlanceTheme( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + GlanceTheme.colors + else + MyAppWidgetGlanceColorScheme.colors + ) { + MyContent() + } + } + } + + @Composable + private fun MyContent() { + // ... + Image( + colorFilter = ColorFilter.tint(GlanceTheme.colors.secondary), + // [START_EXCLUDE] + provider = ImageProvider(R.drawable.ic_logo), + contentDescription = "Example" + // [END_EXCLUDE] + ) + } + // [END android_compose_glance_glancetheming03] + } + + @Composable + fun ShapeExample() { + // Note : android_compose_glance_glancetheming04 is found in button_outline.xml + // [START android_compose_glance_glancetheming05] + GlanceModifier.background( + imageProvider = ImageProvider(R.drawable.button_outline) + ) + // [END android_compose_glance_glancetheming05] + } +} + +object GlanceInnerPadding { + + // [START android_compose_glance_innercornerradius] + /** + * Applies corner radius for views that are visually positioned [widgetPadding]dp inside of the + * widget background. + */ + @Composable + fun GlanceModifier.appWidgetInnerCornerRadius(widgetPadding: Dp): GlanceModifier { + + if (Build.VERSION.SDK_INT < 31) { + return this + } + + val resources = LocalContext.current.resources + // get dimension in float (without rounding). + val px = resources.getDimension(android.R.dimen.system_app_widget_background_radius) + val widgetBackgroundRadiusDpValue = px / resources.displayMetrics.density + if (widgetBackgroundRadiusDpValue < widgetPadding.value) { + return this + } + return this.cornerRadius(Dp(widgetBackgroundRadiusDpValue - widgetPadding.value)) + } + // [END android_compose_glance_innercornerradius] +} + +object GlanceInteroperability { + @Composable + fun Example01() { + // [START android_compose_glance_glanceinteroperability01] + val packageName = LocalContext.current.packageName + Column(modifier = GlanceModifier.fillMaxSize()) { + Text("Isn't that cool?") + AndroidRemoteViews(RemoteViews(packageName, R.layout.example_layout)) + } + // [END android_compose_glance_glanceinteroperability01] + } + + @Composable + fun Example02() { + val packageName = null + + // [START android_compose_glance_glanceinteroperability02] + + AndroidRemoteViews( + remoteViews = RemoteViews(packageName, R.layout.my_container_view), + containerViewId = R.id.example_view + ) { + Column(modifier = GlanceModifier.fillMaxSize()) { + Text("My title") + Text("Maybe a long content...") + } + } + // [END android_compose_glance_glanceinteroperability02] + } +} + +/** + * Dummy interface + */ +interface MyRepository { + + suspend fun saveApple(a: Any) + + companion object { + fun getInstance(): MyRepository { + TODO("Example") + } + } +} + +/**dummy function*/ +private fun RowScope.Button(s: String = "") { + TODO("Not yet implemented") +} + +/** dummy class */ +data class Person(val id: String, val name: String) + +/** dummy class */ +class GlanceSizeModeWidget : GlanceAppWidget() { + override suspend fun provideGlance(context: Context, id: GlanceId) { + TODO("Not yet implemented") + } +} + +/** dummy class */ +class MyAppWidget : GlanceAppWidget() { + override suspend fun provideGlance(context: Context, id: GlanceId) { + TODO("Not yet implemented") + } +} + +/** + * Dummy Interface + */ +sealed interface State { + + object Loading : State + object Error : State + object Completed : State +} + +/** + * Dummy class + */ +class DestinationsRepository { + + lateinit var destinations: StateFlow + + companion object { + fun getInstance(): DestinationsRepository { + TODO("Not yet implemented") + } + } +} + +/** + * Dummy activity for snippet + */ +class NavigationActivity : AppCompatActivity() { + companion object { + val KEY_DESTINATION: String = "destination" + } +} + +/** + * Dummy lambda + */ +private fun submitData() { + TODO("Not yet implemented") +} + +/** + * Dummy broadcast receiver for snippets + */ +class MyReceiver : BroadcastReceiver() { + override fun onReceive(p0: Context?, p1: Intent?) { + TODO("Not yet implemented") + } +} + +/** + * Dummy service for snippets + */ +class SyncService : Service() { + override fun onBind(p0: Intent?): IBinder? { + TODO("Not yet implemented") + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt new file mode 100644 index 000000000..bc16c96a5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt @@ -0,0 +1,269 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.graphics + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.Intent.createChooser +import android.graphics.Bitmap +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat.startActivity +import com.example.compose.snippets.R +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import java.io.File +import kotlin.coroutines.resume +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +@Preview +@Composable +private fun CreateBitmapFromGraphicsLayer() { + // [START android_compose_graphics_layer_bitmap_basics] + val coroutineScope = rememberCoroutineScope() + val graphicsLayer = rememberGraphicsLayer() + Box( + modifier = Modifier + .drawWithContent { + // call record to capture the content in the graphics layer + graphicsLayer.record { + // draw the contents of the composable into the graphics layer + this@drawWithContent.drawContent() + } + // draw the graphics layer on the visible canvas + drawLayer(graphicsLayer) + } + .clickable { + coroutineScope.launch { + val bitmap = graphicsLayer.toImageBitmap() + // do something with the newly acquired bitmap + } + } + .background(Color.White) + ) { + Text("Hello Android", fontSize = 26.sp) + } + // [END android_compose_graphics_layer_bitmap_basics] +} + +@OptIn(ExperimentalPermissionsApi::class) +@Preview +@Composable +fun BitmapFromComposableFullSnippet() { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + val graphicsLayer = rememberGraphicsLayer() + + val writeStorageAccessState = rememberMultiplePermissionsState( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // No permissions are needed on Android 10+ to add files in the shared storage + emptyList() + } else { + listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + ) + + // This logic should live in your ViewModel - trigger a side effect to invoke URI sharing. + // checks permissions granted, and then saves the bitmap from a Picture that is already capturing content + // and shares it with the default share sheet. + fun shareBitmapFromComposable() { + if (writeStorageAccessState.allPermissionsGranted) { + coroutineScope.launch { + val bitmap = graphicsLayer.toImageBitmap() + val uri = bitmap.asAndroidBitmap().saveToDisk(context) + shareBitmap(context, uri) + } + } else if (writeStorageAccessState.shouldShowRationale) { + coroutineScope.launch { + val result = snackbarHostState.showSnackbar( + message = "The storage permission is needed to save the image", + actionLabel = "Grant Access" + ) + + if (result == SnackbarResult.ActionPerformed) { + writeStorageAccessState.launchMultiplePermissionRequest() + } + } + } else { + writeStorageAccessState.launchMultiplePermissionRequest() + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + floatingActionButton = { + FloatingActionButton(onClick = { + shareBitmapFromComposable() + }) { + Icon(Icons.Default.Share, "share") + } + } + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .drawWithCache { + onDrawWithContent { + graphicsLayer.record { + this@onDrawWithContent.drawContent() + } + drawLayer(graphicsLayer) + } + } + + ) { + ScreenContentToCapture() + } + } +} + +@Composable +private fun ScreenContentToCapture() { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .background( + Brush.linearGradient( + listOf( + Color(0xFFF5D5C0), + Color(0xFFF8E8E3) + ) + ) + ) + ) { + Image( + painterResource(id = R.drawable.sunset), + contentDescription = null, + modifier = Modifier + .aspectRatio(1f) + .padding(32.dp), + contentScale = ContentScale.Crop + ) + Text( + "Into the Ocean depths", + fontSize = 18.sp + ) + } +} + +private suspend fun Bitmap.saveToDisk(context: Context): Uri { + val file = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + "screenshot-${System.currentTimeMillis()}.png" + ) + + file.writeBitmap(this, Bitmap.CompressFormat.PNG, 100) + + return scanFilePath(context, file.path) ?: throw Exception("File could not be saved") +} + +/** + * We call [MediaScannerConnection] to index the newly created image inside MediaStore to be visible + * for other apps, as well as returning its [MediaStore] Uri + */ +private suspend fun scanFilePath(context: Context, filePath: String): Uri? { + return suspendCancellableCoroutine { continuation -> + MediaScannerConnection.scanFile( + context, + arrayOf(filePath), + arrayOf("image/png") + ) { _, scannedUri -> + if (scannedUri == null) { + continuation.cancel(Exception("File $filePath could not be scanned")) + } else { + continuation.resume(scannedUri) + } + } + } +} + +private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) { + outputStream().use { out -> + bitmap.compress(format, quality, out) + out.flush() + } +} + +private fun shareBitmap(context: Context, uri: Uri) { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(context, createChooser(intent, "Share your image"), null) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/BrushExampleSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/BrushExampleSnippets.kt new file mode 100644 index 000000000..23a6a58ee --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/BrushExampleSnippets.kt @@ -0,0 +1,333 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.graphics + +import android.graphics.RuntimeShader +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageShader +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.RadialGradientShader +import androidx.compose.ui.graphics.Shader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.drawscope.inset +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.R +import org.intellij.lang.annotations.Language + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/** + * The snippets in this file relate to the documentation at + * https://developr.android.com/jetpack/compose/graphics/draw/brush + */ +@Composable +fun BrushExamplesScreen() { + Column(Modifier.verticalScroll(rememberScrollState())) { + GraphicsBrushCanvasUsage() + GraphicsBrushColorStopUsage() + GraphicsBrushTileMode() + GraphicsBrushSize() + GraphicsBrushSizeRecreationExample() + GraphicsImageBrush() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ShaderBrushExample() + } + } +} + +@Preview +@Composable +fun GraphicsBrushCanvasUsage() { + // [START android_compose_brush_basic_canvas] + val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue)) + Canvas( + modifier = Modifier.size(200.dp), + onDraw = { + drawCircle(brush) + } + ) + // [END android_compose_brush_basic_canvas] +} + +@Preview +@Composable +fun GraphicsBrushColorStopUsage() { + // [START android_compose_brush_color_stop] + val colorStops = arrayOf( + 0.0f to Color.Yellow, + 0.2f to Color.Red, + 1f to Color.Blue + ) + Box( + modifier = Modifier + .requiredSize(200.dp) + .background(Brush.horizontalGradient(colorStops = colorStops)) + ) + // [END android_compose_brush_color_stop] +} + +@Preview +@Composable +fun GraphicsBrushTileMode() { + // [START android_compose_brush_tile_mode] + val listColors = listOf(Color.Yellow, Color.Red, Color.Blue) + val tileSize = with(LocalDensity.current) { + 50.dp.toPx() + } + Box( + modifier = Modifier + .requiredSize(200.dp) + .background( + Brush.horizontalGradient( + listColors, + endX = tileSize, + tileMode = TileMode.Repeated + ) + ) + ) + // [END android_compose_brush_tile_mode] +} + +@Composable +@Preview +fun GraphicsBrushSize() { + // [START android_compose_graphics_brush_size] + val listColors = listOf(Color.Yellow, Color.Red, Color.Blue) + val customBrush = remember { + object : ShaderBrush() { + override fun createShader(size: Size): Shader { + return LinearGradientShader( + colors = listColors, + from = Offset.Zero, + to = Offset(size.width / 4f, 0f), + tileMode = TileMode.Mirror + ) + } + } + } + Box( + modifier = Modifier + .requiredSize(200.dp) + .background(customBrush) + ) + // [END android_compose_graphics_brush_size] +} + +@Composable +@Preview +fun GraphicsBrushSizeRadialGradientBefore() { + // [START android_compose_graphics_brush_size_radial_before] + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.radialGradient( + listOf(Color(0xFF2be4dc), Color(0xFF243484)) + ) + ) + ) + // [END android_compose_graphics_brush_size_radial_before] +} + +@Preview +@Composable +fun GraphicsBrushSizeRadialGradientAfter() { + // [START android_compose_graphics_brush_size_radial_after] + val largeRadialGradient = object : ShaderBrush() { + override fun createShader(size: Size): Shader { + val biggerDimension = maxOf(size.height, size.width) + return RadialGradientShader( + colors = listOf(Color(0xFF2be4dc), Color(0xFF243484)), + center = size.center, + radius = biggerDimension / 2f, + colorStops = listOf(0f, 0.95f) + ) + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(largeRadialGradient) + ) + // [END android_compose_graphics_brush_size_radial_after] +} + +@Preview +@Composable +fun GraphicsBrushSizeRecreationExample() { + // [START android_compose_graphics_brush_recreation] + val colorStops = arrayOf( + 0.0f to Color.Yellow, + 0.2f to Color.Red, + 1f to Color.Blue + ) + val brush = Brush.horizontalGradient(colorStops = colorStops) + Box( + modifier = Modifier + .requiredSize(200.dp) + .drawBehind { + drawRect(brush = brush) // will allocate a shader to occupy the 200 x 200 dp drawing area + inset(10f) { + /* Will allocate a shader to occupy the 180 x 180 dp drawing area as the + inset scope reduces the drawing area by 10 pixels on the left, top, right, + bottom sides */ + drawRect(brush = brush) + inset(5f) { + /* will allocate a shader to occupy the 170 x 170 dp drawing area as the + inset scope reduces the drawing area by 5 pixels on the left, top, + right, bottom sides */ + drawRect(brush = brush) + } + } + } + ) + // [END android_compose_graphics_brush_recreation] +} + +@Preview +@Composable +fun GraphicsImageBrush() { + // [START android_compose_graphics_brush_image] + val imageBrush = + ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.dog))) + + // Use ImageShader Brush with background + Box( + modifier = Modifier + .requiredSize(200.dp) + .background(imageBrush) + ) + + // Use ImageShader Brush with TextStyle + Text( + text = "Hello Android!", + style = TextStyle( + brush = imageBrush, + fontWeight = FontWeight.ExtraBold, + fontSize = 36.sp + ) + ) + + // Use ImageShader Brush with DrawScope#drawCircle() + Canvas(onDraw = { + drawCircle(imageBrush) + }, modifier = Modifier.size(200.dp)) + // [END android_compose_graphics_brush_image] +} + +// [START android_compose_brush_custom_shader] +@Language("AGSL") +val CUSTOM_SHADER = """ + uniform float2 resolution; + layout(color) uniform half4 color; + layout(color) uniform half4 color2; + + half4 main(in float2 fragCoord) { + float2 uv = fragCoord/resolution.xy; + + float mixValue = distance(uv, vec2(0, 1)); + return mix(color, color2, mixValue); + } +""".trimIndent() +// [END android_compose_brush_custom_shader] + +// [START android_compose_brush_custom_shader_usage] +val Coral = Color(0xFFF3A397) +val LightYellow = Color(0xFFF8EE94) + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@Composable +@Preview +fun ShaderBrushExample() { + Box( + modifier = Modifier + .drawWithCache { + val shader = RuntimeShader(CUSTOM_SHADER) + val shaderBrush = ShaderBrush(shader) + shader.setFloatUniform("resolution", size.width, size.height) + onDrawBehind { + shader.setColorUniform( + "color", + android.graphics.Color.valueOf( + LightYellow.red, LightYellow.green, + LightYellow + .blue, + LightYellow.alpha + ) + ) + shader.setColorUniform( + "color2", + android.graphics.Color.valueOf( + Coral.red, + Coral.green, + Coral.blue, + Coral.alpha + ) + ) + drawRect(shaderBrush) + } + } + .fillMaxWidth() + .height(200.dp) + ) +} +// [END android_compose_brush_custom_shader_usage] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/CanvasSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/CanvasSnippets.kt new file mode 100644 index 000000000..4b53db6e6 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/CanvasSnippets.kt @@ -0,0 +1,370 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.graphics + +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.OvalShape +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PointMode +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.drawscope.inset +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.toSize +import com.example.compose.snippets.R + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +@Preview +@Composable +fun BasicCanvasUsage() { + // [START android_compose_graphics_canvas_basic] + Spacer( + modifier = Modifier + .fillMaxSize() + .drawBehind { + // this = DrawScope + } + ) + // [END android_compose_graphics_canvas_basic] +} + +@Preview +@Composable +fun CanvasCircleExample() { + // [START android_compose_graphics_canvas_circle] + Canvas(modifier = Modifier.fillMaxSize()) { + val canvasQuadrantSize = size / 2F + drawRect( + color = Color.Magenta, + size = canvasQuadrantSize + ) + } + // [END android_compose_graphics_canvas_circle] +} + +@Preview +@Composable +fun CanvasDrawDiagonalLineExample() { + // [START android_compose_graphics_canvas_diagonal_line] + Canvas(modifier = Modifier.fillMaxSize()) { + val canvasWidth = size.width + val canvasHeight = size.height + drawLine( + start = Offset(x = canvasWidth, y = 0f), + end = Offset(x = 0f, y = canvasHeight), + color = Color.Blue + ) + } + // [END android_compose_graphics_canvas_diagonal_line] +} + +@Preview +@Composable +fun CanvasTransformationScale() { + // [START android_compose_graphics_canvas_scale] + Canvas(modifier = Modifier.fillMaxSize()) { + scale(scaleX = 10f, scaleY = 15f) { + drawCircle(Color.Blue, radius = 20.dp.toPx()) + } + } + // [END android_compose_graphics_canvas_scale] +} + +@Preview +@Composable +fun CanvasTransformationTranslate() { + // [START android_compose_graphics_canvas_translate] + Canvas(modifier = Modifier.fillMaxSize()) { + translate(left = 100f, top = -300f) { + drawCircle(Color.Blue, radius = 200.dp.toPx()) + } + } + // [END android_compose_graphics_canvas_translate] +} + +@Preview +@Composable +fun CanvasTransformationRotate() { + // [START android_compose_graphics_canvas_rotate] + Canvas(modifier = Modifier.fillMaxSize()) { + rotate(degrees = 45F) { + drawRect( + color = Color.Gray, + topLeft = Offset(x = size.width / 3F, y = size.height / 3F), + size = size / 3F + ) + } + } + // [END android_compose_graphics_canvas_rotate] +} + +@Preview +@Composable +fun CanvasTransformationInset() { + // [START android_compose_graphics_canvas_inset] + Canvas(modifier = Modifier.fillMaxSize()) { + val canvasQuadrantSize = size / 2F + inset(horizontal = 50f, vertical = 30f) { + drawRect(color = Color.Green, size = canvasQuadrantSize) + } + } + // [END android_compose_graphics_canvas_inset] +} + +@Preview +@Composable +fun CanvasMultipleTransformations() { + // [START android_compose_graphics_canvas_multiple_transforms] + Canvas(modifier = Modifier.fillMaxSize()) { + withTransform({ + translate(left = size.width / 5F) + rotate(degrees = 45F) + }) { + drawRect( + color = Color.Gray, + topLeft = Offset(x = size.width / 3F, y = size.height / 3F), + size = size / 3F + ) + } + } + // [END android_compose_graphics_canvas_multiple_transforms] +} + +@Preview +@Composable +fun CanvasDrawText() { + // [START android_compose_graphics_canvas_draw_text] + val textMeasurer = rememberTextMeasurer() + + Canvas(modifier = Modifier.fillMaxSize()) { + drawText(textMeasurer, "Hello") + } + // [END android_compose_graphics_canvas_draw_text] +} + +@Preview +@Composable +fun CanvasDrawImage() { + // [START android_compose_graphics_canvas_draw_image] + val dogImage = ImageBitmap.imageResource(id = R.drawable.dog) + + Canvas(modifier = Modifier.fillMaxSize(), onDraw = { + drawImage(dogImage) + }) + // [END android_compose_graphics_canvas_draw_image] +} + +@Preview +@Composable +fun CanvasDrawPath() { + // [START android_compose_graphics_canvas_draw_path] + Spacer( + modifier = Modifier + .drawWithCache { + val path = Path() + path.moveTo(0f, 0f) + path.lineTo(size.width / 2f, size.height / 2f) + path.lineTo(size.width, 0f) + path.close() + onDrawBehind { + drawPath(path, Color.Magenta, style = Stroke(width = 10f)) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_canvas_draw_path] +} + +@Preview +@Composable +fun CanvasMeasureText() { + val pinkColor = Color(0xFFF48FB1) + val longTextSample = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + // [START android_compose_graphics_canvas_draw_text_measure] + val textMeasurer = rememberTextMeasurer() + + Spacer( + modifier = Modifier + .drawWithCache { + val measuredText = + textMeasurer.measure( + AnnotatedString(longTextSample), + constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()), + style = TextStyle(fontSize = 18.sp) + ) + + onDrawBehind { + drawRect(pinkColor, size = measuredText.size.toSize()) + drawText(measuredText) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_canvas_draw_text_measure] +} + +@Preview +@Composable +fun CanvasMeasureTextOverflow() { + val pinkColor = Color(0xFFF48FB1) + val longTextSample = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + // [START android_compose_graphics_canvas_draw_text_measure_ellipsis] + val textMeasurer = rememberTextMeasurer() + + Spacer( + modifier = Modifier + .drawWithCache { + val measuredText = + textMeasurer.measure( + AnnotatedString(longTextSample), + constraints = Constraints.fixed( + width = (size.width / 3f).toInt(), + height = (size.height / 3f).toInt() + ), + overflow = TextOverflow.Ellipsis, + style = TextStyle(fontSize = 18.sp) + ) + + onDrawBehind { + drawRect(pinkColor, size = measuredText.size.toSize()) + drawText(measuredText) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_canvas_draw_text_measure_ellipsis] +} + +@Preview +@Composable +fun CanvasDrawIntoCanvas() { + // [START android_compose_graphics_canvas_draw_into_canvas] + val drawable = ShapeDrawable(OvalShape()) + Spacer( + modifier = Modifier + .drawWithContent { + drawIntoCanvas { canvas -> + drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt()) + drawable.draw(canvas.nativeCanvas) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_canvas_draw_into_canvas] +} + +@Preview +@Composable +fun CanvasDrawShape() { + // [START android_compose_graphics_draw_shape] + val purpleColor = Color(0xFFBA68C8) + Canvas( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + onDraw = { + drawCircle(purpleColor) + } + ) + // [END android_compose_graphics_draw_shape] +} + +@Preview +@Composable +fun CanvasDrawOtherShapes() { + val purpleColor = Color(0xFFBA68C8) + Canvas( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + onDraw = { + drawPoints( + listOf( + Offset(0f, 0f), + Offset(size.width / 3f, size.height / 2f), + Offset(size.width / 2f, size.height / 5f), + Offset(size.width, size.height) + ), + color = purpleColor, + pointMode = PointMode.Points, strokeWidth = 10.dp.toPx() + ) + } + ) +} + +@Preview +@Composable +fun CanvasTransformationScaleAnim() { + val animatable = remember { + Animatable(1f) + } + LaunchedEffect(Unit) { + animatable.animateTo(10f, animationSpec = tween(3000, 3000, easing = LinearEasing)) + } + Canvas(modifier = Modifier.fillMaxSize()) { + scale(scaleX = animatable.value, scaleY = animatable.value * 1.5f) { + drawCircle(Color.Blue, radius = 20.dp.toPx()) + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt new file mode 100644 index 000000000..0493012ed --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt @@ -0,0 +1,674 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.graphics + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.DrawModifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.R +import kotlin.random.Random + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +@Composable +@Preview +fun ModifierDrawWithContent() { + // [START android_compose_graphics_modifiers_drawWithContent] + var pointerOffset by remember { + mutableStateOf(Offset(0f, 0f)) + } + Column( + modifier = Modifier + .fillMaxSize() + .pointerInput("dragging") { + detectDragGestures { change, dragAmount -> + pointerOffset += dragAmount + } + } + .onSizeChanged { + pointerOffset = Offset(it.width / 2f, it.height / 2f) + } + .drawWithContent { + drawContent() + // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI. + drawRect( + Brush.radialGradient( + listOf(Color.Transparent, Color.Black), + center = pointerOffset, + radius = 100.dp.toPx(), + ) + ) + } + ) { + // Your composables here + } + // [END android_compose_graphics_modifiers_drawWithContent] +} + +@Composable +@Preview +fun ModifierDrawBehind() { + // [START android_compose_graphics_modifiers_drawBehind] + Text( + "Hello Compose!", + modifier = Modifier + .drawBehind { + drawRoundRect( + Color(0xFFBBAAEE), + cornerRadius = CornerRadius(10.dp.toPx()) + ) + } + .padding(4.dp) + ) + // [END android_compose_graphics_modifiers_drawBehind] +} + +@Composable +@Preview +fun ModifierDrawWithCache() { + // [START android_compose_graphics_modifiers_drawWithCache] + Text( + "Hello Compose!", + modifier = Modifier + .drawWithCache { + val brush = Brush.linearGradient( + listOf( + Color(0xFF9E82F0), + Color(0xFF42A5F5) + ) + ) + onDrawBehind { + drawRoundRect( + brush, + cornerRadius = CornerRadius(10.dp.toPx()) + ) + } + } + ) + // [END android_compose_graphics_modifiers_drawWithCache] +} + +@Composable +@Preview +fun ModifierGraphicsLayerModifierScale() { + // [START android_compose_graphics_modifiers_graphicsLayer_scale] + Image( + painter = painterResource(id = R.drawable.sunset), + contentDescription = "Sunset", + modifier = Modifier + .graphicsLayer { + this.scaleX = 1.2f + this.scaleY = 0.8f + } + ) + // [END android_compose_graphics_modifiers_graphicsLayer_scale] +} + +@Composable +@Preview +fun ModifierGraphicsLayerModifierTranslation() { + // [START android_compose_graphics_modifiers_graphicsLayer_translation] + Image( + painter = painterResource(id = R.drawable.sunset), + contentDescription = "Sunset", + modifier = Modifier + .graphicsLayer { + this.translationX = 100.dp.toPx() + this.translationY = 10.dp.toPx() + } + ) + // [END android_compose_graphics_modifiers_graphicsLayer_translation] +} + +@Composable +@Preview +fun ModifierGraphicsLayerModifierRotation() { + // [START android_compose_graphics_modifiers_graphicsLayer_rotation] + Image( + painter = painterResource(id = R.drawable.sunset), + contentDescription = "Sunset", + modifier = Modifier + .graphicsLayer { + this.rotationX = 90f + this.rotationY = 275f + this.rotationZ = 180f + } + ) + // [END android_compose_graphics_modifiers_graphicsLayer_rotation] +} + +@Preview +@Composable +fun ModifierGraphicsLayerModifierOrigin() { + // [START android_compose_graphics_modifiers_graphicsLayer_origin] + Image( + painter = painterResource(id = R.drawable.sunset), + contentDescription = "Sunset", + modifier = Modifier + .graphicsLayer { + this.transformOrigin = TransformOrigin(0f, 0f) + this.rotationX = 90f + this.rotationY = 275f + this.rotationZ = 180f + } + ) + // [END android_compose_graphics_modifiers_graphicsLayer_origin] +} + +@Preview +@Composable +fun ModifierGraphicsLayerModifierClipShape() { + // [START android_compose_graphics_modifiers_graphicsLayer_clip_shape] + Column(modifier = Modifier.padding(16.dp)) { + Box( + modifier = Modifier + .size(200.dp) + .graphicsLayer { + clip = true + shape = CircleShape + } + .background(Color(0xFFF06292)) + ) { + Text( + "Hello Compose", + style = TextStyle(color = Color.Black, fontSize = 46.sp), + modifier = Modifier.align(Alignment.Center) + ) + } + Box( + modifier = Modifier + .size(200.dp) + .clip(CircleShape) + .background(Color(0xFF4DB6AC)) + ) + } + // [END android_compose_graphics_modifiers_graphicsLayer_clip_shape] + + // [START android_compose_graphics_modifiers_graphicsLayer_clip_shape_2] + Column(modifier = Modifier.padding(16.dp)) { + Box( + modifier = Modifier + .clip(RectangleShape) + .size(200.dp) + .border(2.dp, Color.Black) + .graphicsLayer { + clip = true + shape = CircleShape + translationY = 50.dp.toPx() + } + .background(Color(0xFFF06292)) + ) { + Text( + "Hello Compose", + style = TextStyle(color = Color.Black, fontSize = 46.sp), + modifier = Modifier.align(Alignment.Center) + ) + } + + Box( + modifier = Modifier + .size(200.dp) + .clip(RoundedCornerShape(500.dp)) + .background(Color(0xFF4DB6AC)) + ) + } + // [END android_compose_graphics_modifiers_graphicsLayer_clip_shape_2] +} + +@Preview +@Composable +fun ModifierGraphicsLayerAlpha() { + // [START android_compose_graphics_modifiers_graphicsLayer_alpha] + Image( + painter = painterResource(id = R.drawable.sunset), + contentDescription = "clock", + modifier = Modifier + .graphicsLayer { + this.alpha = 0.5f + } + ) + // [END android_compose_graphics_modifiers_graphicsLayer_alpha] +} + +@Preview +@Composable +fun ModifierGraphicsLayerCompositingStrategy() { + // [START android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] + + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = "Dog", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(120.dp) + .aspectRatio(1f) + .background( + Brush.linearGradient( + listOf( + Color(0xFFC5E1A5), + Color(0xFF80DEEA) + ) + ) + ) + .padding(8.dp) + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen + } + .drawWithCache { + val path = Path() + path.addOval( + Rect( + topLeft = Offset.Zero, + bottomRight = Offset(size.width, size.height) + ) + ) + onDrawWithContent { + clipPath(path) { + // this draws the actual image - if you don't call drawContent, it wont + // render anything + this@onDrawWithContent.drawContent() + } + val dotSize = size.width / 8f + // Clip a white border for the content + drawCircle( + Color.Black, + radius = dotSize, + center = Offset( + x = size.width - dotSize, + y = size.height - dotSize + ), + blendMode = BlendMode.Clear + ) + // draw the red circle indication + drawCircle( + Color(0xFFEF5350), radius = dotSize * 0.8f, + center = Offset( + x = size.width - dotSize, + y = size.height - dotSize + ) + ) + } + } + ) + // [END android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] +} + +@Preview +// [START android_compose_graphics_modifier_compositing_strategy_differences] +@Composable +fun CompositingStrategyExamples() { + Column( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + ) { + // Does not clip content even with a graphics layer usage here. By default, graphicsLayer + // does not allocate + rasterize content into a separate layer but instead is used + // for isolation. That is draw invalidations made outside of this graphicsLayer will not + // re-record the drawing instructions in this composable as they have not changed + Canvas( + modifier = Modifier + .graphicsLayer() + .size(100.dp) // Note size of 100 dp here + .border(2.dp, color = Color.Blue) + ) { + // ... and drawing a size of 200 dp here outside the bounds + drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) + } + + Spacer(modifier = Modifier.size(300.dp)) + + /* Clips content as alpha usage here creates an offscreen buffer to rasterize content + into first then draws to the original destination */ + Canvas( + modifier = Modifier + // force to an offscreen buffer + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .size(100.dp) // Note size of 100 dp here + .border(2.dp, color = Color.Blue) + ) { + /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the + content gets clipped */ + drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) + } + } +} +// [END android_compose_graphics_modifier_compositing_strategy_differences] + +// [START android_compose_graphics_modifier_compositing_strategy_modulate_alpha] +@Preview +@Composable +fun CompositingStrategy_ModulateAlpha() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp) + ) { + // Base drawing, no alpha applied + Canvas( + modifier = Modifier.size(200.dp) + ) { + drawSquares() + } + + Spacer(modifier = Modifier.size(36.dp)) + + // Alpha 0.5f applied to whole composable + Canvas( + modifier = Modifier + .size(200.dp) + .graphicsLayer { + alpha = 0.5f + } + ) { + drawSquares() + } + Spacer(modifier = Modifier.size(36.dp)) + + // 0.75f alpha applied to each draw call when using ModulateAlpha + Canvas( + modifier = Modifier + .size(200.dp) + .graphicsLayer { + compositingStrategy = CompositingStrategy.ModulateAlpha + alpha = 0.75f + } + ) { + drawSquares() + } + } +} + +private fun DrawScope.drawSquares() { + + val size = Size(100.dp.toPx(), 100.dp.toPx()) + drawRect(color = Red, size = size) + drawRect( + color = Purple, size = size, + topLeft = Offset(size.width / 4f, size.height / 4f) + ) + drawRect( + color = Yellow, size = size, + topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) + ) +} + +val Purple = Color(0xFF7E57C2) +val Yellow = Color(0xFFFFCA28) +val Red = Color(0xFFEF5350) +// [END android_compose_graphics_modifier_compositing_strategy_modulate_alpha] + +// [START android_compose_graphics_modifier_flipped] +class FlippedModifier : DrawModifier { + override fun ContentDrawScope.draw() { + scale(1f, -1f) { + this@draw.drawContent() + } + } +} + +fun Modifier.flipped() = this.then(FlippedModifier()) +// [END android_compose_graphics_modifier_flipped] + +@Preview +@Composable +fun ModifierGraphicsFlippedUsage() { + // [START android_compose_graphics_modifier_flipped_usage] + Text( + "Hello Compose!", + modifier = Modifier + .flipped() + ) + // [END android_compose_graphics_modifier_flipped_usage] +} + +// [START android_compose_graphics_faded_edge_example] +@Composable +fun FadedEdgeBox(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Box( + modifier = modifier + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .drawWithContent { + drawContent() + drawRect( + brush = Brush.verticalGradient( + listOf(Color.Black, Color.Transparent) + ), + blendMode = BlendMode.DstIn + ) + } + ) { + content() + } +} +// [END android_compose_graphics_faded_edge_example] +@Preview +@Composable +private fun FadingLazyComments() { + FadedEdgeBox( + modifier = Modifier + .padding(32.dp) + .height(300.dp) + .fillMaxWidth() + ) { + LazyColumn { + items(listComments, key = { it.key }) { + ListCommentItem(it) + } + item { + Spacer(Modifier.height(100.dp)) + } + } + } +} + +@Composable +private fun ListCommentItem(it: Comment) { + Row(modifier = Modifier.padding(bottom = 8.dp)) { + val strokeWidthPx = with(LocalDensity.current) { + 2.dp.toPx() + } + Avatar(strokeWidth = strokeWidthPx, modifier = Modifier.size(48.dp)) { + Image( + painter = painterResource(id = it.avatar), + contentScale = ContentScale.Crop, + contentDescription = null + ) + } + Spacer(Modifier.width(6.dp)) + Text( + it.text, + fontSize = 20.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } +} + +data class Comment( + val avatar: Int, + val text: String, + val key: Int = Random.nextInt() +) + +val listComments = listOf( + Comment(R.drawable.dog, "Woof 🐶"), + Comment(R.drawable.froyo, "I love ice cream..."), + Comment(R.drawable.donut, "Mmmm delicious"), + Comment(R.drawable.cupcake, "I love cupcakes"), + Comment(R.drawable.gingerbread, "🍪🍪❤️"), + Comment(R.drawable.eclair, "Where do I get the recipe?"), + Comment(R.drawable.froyo, "🍦The ice cream is BEST"), +) + +// [START android_compose_graphics_stacked_clipped_avatars] +@Composable +fun Avatar( + strokeWidth: Float, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val stroke = remember(strokeWidth) { + Stroke(width = strokeWidth) + } + Box( + modifier = modifier + .drawWithContent { + drawContent() + drawCircle( + Color.Black, + size.minDimension / 2, + size.center, + style = stroke, + blendMode = BlendMode.Clear + ) + } + .clip(CircleShape) + ) { + content() + } +} + +@Preview +@Composable +private fun StackedAvatars() { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.linearGradient( + colors = listOf( + Color.Magenta.copy(alpha = 0.5f), + Color.Blue.copy(alpha = 0.5f) + ) + ) + ) + ) { + val size = 80.dp + val strokeWidth = 2.dp + val strokeWidthPx = with(LocalDensity.current) { + strokeWidth.toPx() + } + val sizeModifier = Modifier.size(size) + val avatars = listOf( + R.drawable.cupcake, + R.drawable.donut, + R.drawable.eclair, + R.drawable.froyo, + R.drawable.gingerbread, + R.drawable.dog + ) + val width = ((size / 2) + strokeWidth * 2) * (avatars.size + 1) + Box( + modifier = Modifier + .size(width, size) + .graphicsLayer { + // Use an offscreen buffer as underdraw protection when + // using blendmodes that clear destination pixels + compositingStrategy = CompositingStrategy.Offscreen + } + .align(Alignment.Center), + ) { + var offset = 0.dp + for (avatar in avatars) { + Avatar( + strokeWidth = strokeWidthPx, + modifier = sizeModifier.offset(offset) + ) { + Image( + painter = painterResource(id = avatar), + contentScale = ContentScale.Crop, + contentDescription = null + ) + } + offset += size / 2 + } + } + } +} +// [END android_compose_graphics_stacked_clipped_avatars] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/Shadows.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/Shadows.kt new file mode 100644 index 000000000..8e2b208b5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/Shadows.kt @@ -0,0 +1,616 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.graphics + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.EaseInOut +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.draw.innerShadow +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.ui.theme.SnippetsTheme +import kotlinx.coroutines.delay + +@Preview(showBackground = true) +// [START android_compose_graphics_simple_shadow] +@Composable +fun ElevationBasedShadow() { + Box( + modifier = Modifier.aspectRatio(1f).fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Box( + Modifier + .size(100.dp, 100.dp) + .shadow(10.dp, RectangleShape) + .background(Color.White) + ) + } +} +// [END android_compose_graphics_simple_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF +) +// [START android_compose_graphics_simple_drop_shadow] +@Composable +fun SimpleDropShadowUsage() { + Box(Modifier.fillMaxSize()) { + Box( + Modifier + .width(300.dp) + .height(300.dp) + .dropShadow( + shape = RoundedCornerShape(20.dp), + shadow = Shadow( + radius = 10.dp, + spread = 6.dp, + color = Color(0x40000000), + offset = DpOffset(x = 4.dp, 4.dp) + ) + ) + .align(Alignment.Center) + .background( + color = Color.White, + shape = RoundedCornerShape(20.dp) + ) + ) { + Text( + "Drop Shadow", + modifier = Modifier.align(Alignment.Center), + fontSize = 32.sp + ) + } + } +} +// [END android_compose_graphics_simple_drop_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF +) +// [START android_compose_graphics_simple_inner_shadow] +@Composable +fun SimpleInnerShadowUsage() { + Box(Modifier.fillMaxSize()) { + Box( + Modifier + .width(300.dp) + .height(200.dp) + .align(Alignment.Center) + // note that the background needs to be defined before defining the inner shadow + .background( + color = Color.White, + shape = RoundedCornerShape(20.dp) + ) + .innerShadow( + shape = RoundedCornerShape(20.dp), + shadow = Shadow( + radius = 10.dp, + spread = 2.dp, + color = Color(0x40000000), + offset = DpOffset(x = 6.dp, 7.dp) + ) + ) + + ) { + Text( + "Inner Shadow", + modifier = Modifier.align(Alignment.Center), + fontSize = 32.sp + ) + } + } +} +// [END android_compose_graphics_simple_inner_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFF232323 +) +// [START android_compose_graphics_realistic_shadow] +@Composable +fun RealisticShadows() { + Box(Modifier.fillMaxSize()) { + val dropShadowColor1 = Color(0xB3000000) + val dropShadowColor2 = Color(0x66000000) + + val innerShadowColor1 = Color(0xCC000000) + val innerShadowColor2 = Color(0xFF050505) + val innerShadowColor3 = Color(0x40FFFFFF) + val innerShadowColor4 = Color(0x1A050505) + Box( + Modifier + .width(300.dp) + .height(200.dp) + .align(Alignment.Center) + .dropShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 40.dp, + spread = 0.dp, + color = dropShadowColor1, + offset = DpOffset(x = 2.dp, 8.dp) + ) + ) + .dropShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 4.dp, + spread = 0.dp, + color = dropShadowColor2, + offset = DpOffset(x = 0.dp, 4.dp) + ) + ) + // note that the background needs to be defined before defining the inner shadow + .background( + color = Color.Black, + shape = RoundedCornerShape(100.dp) + ) +// // + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 12.dp, + spread = 3.dp, + color = innerShadowColor1, + offset = DpOffset(x = 6.dp, 6.dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 4.dp, + spread = 1.dp, + color = Color.White, + offset = DpOffset(x = 5.dp, 5.dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 12.dp, + spread = 5.dp, + color = innerShadowColor2, + offset = DpOffset(x = (-3).dp, (-12).dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 3.dp, + spread = 10.dp, + color = innerShadowColor3, + offset = DpOffset(x = 0.dp, 0.dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(100.dp), + shadow = Shadow( + radius = 3.dp, + spread = 9.dp, + color = innerShadowColor4, + offset = DpOffset(x = 1.dp, 1.dp) + ) + ) + + ) { + Text( + "Realistic Shadows", + modifier = Modifier.align(Alignment.Center), + fontSize = 24.sp, + color = Color.White + ) + } + } +} +// [END android_compose_graphics_realistic_shadow] + +// Define breathing states +enum class BreathingState { + Inhaling, + Exhaling +} + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF +) +@Composable +fun GradientBasedShadowAnimation() { + SnippetsTheme { + val colors = listOf( + Color(0xFF4cc9f0), + Color(0xFFf72585), + Color(0xFFb5179e), + Color(0xFF7209b7), + Color(0xFF560bad), + Color(0xFF480ca8), + Color(0xFF3a0ca3), + Color(0xFF3f37c9), + Color(0xFF4361ee), + Color(0xFF4895ef), + Color(0xFF4cc9f0) + ) + + // .. + + // State for the breathing animation + var breathingState by remember { mutableStateOf(BreathingState.Inhaling) } + + // Create transition based on breathing state + val transition = updateTransition( + targetState = breathingState, + label = "breathing_transition" + ) + + // Animate spread based on breathing state + val animatedSpread by transition.animateFloat( + transitionSpec = { + tween( + durationMillis = 5000, + easing = FastOutSlowInEasing + ) + }, + label = "spread_animation" + ) { state -> + when (state) { + BreathingState.Inhaling -> 10f + BreathingState.Exhaling -> 2f + } + } + + // Animate alpha based on breathing state (optional) + val animatedAlpha by transition.animateFloat( + transitionSpec = { + tween( + durationMillis = 2000, + easing = FastOutSlowInEasing + ) + }, + label = "alpha_animation" + ) { state -> + when (state) { + BreathingState.Inhaling -> 1f + BreathingState.Exhaling -> 1f + } + } + + // Get text based on current state + val breathingText = when (breathingState) { + BreathingState.Inhaling -> "Inhale" + BreathingState.Exhaling -> "Exhale" + } + + // Switch states when animation completes + LaunchedEffect(breathingState) { + delay(5000) // Wait for animation to complete + breathingState = when (breathingState) { + BreathingState.Inhaling -> BreathingState.Exhaling + BreathingState.Exhaling -> BreathingState.Inhaling + } + } + + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + // [START android_compose_graphics_gradient_shadow] + Box( + modifier = Modifier + .width(240.dp) + .height(200.dp) + .dropShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 10.dp, + spread = animatedSpread.dp, + brush = Brush.sweepGradient( + colors + ), + offset = DpOffset(x = 0.dp, y = 0.dp), + alpha = animatedAlpha + ) + ) + .clip(RoundedCornerShape(70.dp)) + .background(Color(0xEDFFFFFF)), + contentAlignment = Alignment.Center + ) { + Text( + text = breathingText, + color = Color.Black, + style = MaterialTheme.typography.bodyLarge + ) + } + // [END android_compose_graphics_gradient_shadow] + } + } +} + +@Preview +// [START android_compose_graphics_neumorphic_shadow] +@Composable +fun NeumorphicRaisedButton( + shape: RoundedCornerShape = RoundedCornerShape(30.dp) +) { + val bgColor = Color(0xFFe0e0e0) + val lightShadow = Color(0xFFFFFFFF) + val darkShadow = Color(0xFFb1b1b1) + val upperOffset = -10.dp + val lowerOffset = 10.dp + val radius = 15.dp + val spread = 0.dp + Box( + modifier = Modifier + .fillMaxSize() + .background(bgColor) + .wrapContentSize(Alignment.Center) + .size(240.dp) + .dropShadow( + shape, + shadow = Shadow( + radius = radius, + color = lightShadow, + spread = spread, + offset = DpOffset(upperOffset, upperOffset) + ), + ) + .dropShadow( + shape, + shadow = Shadow( + radius = radius, + color = darkShadow, + spread = spread, + offset = DpOffset(lowerOffset, lowerOffset) + ), + + ) + .background(bgColor, shape) + ) +} +// [END android_compose_graphics_neumorphic_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFFFFF +) +// [START android_compose_graphics_animated_shadow] +@Composable +fun AnimatedColoredShadows() { + SnippetsTheme { + Box(Modifier.fillMaxSize()) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + // Create transition with pressed state + val transition = updateTransition( + targetState = isPressed, + label = "button_press_transition" + ) + + fun buttonPressAnimation() = tween( + durationMillis = 400, + easing = EaseInOut + ) + + // Animate all properties using the transition + val shadowAlpha by transition.animateFloat( + label = "shadow_alpha", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) 0f else 1f + } + // [START_EXCLUDE] + val innerShadowAlpha by transition.animateFloat( + label = "shadow_alpha", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) 1f else 0f + } + + val blueDropShadowColor = Color(0x5C007AFF) + + val darkBlueDropShadowColor = Color(0x66007AFF) + + val greyInnerShadowColor1 = Color(0x1A007AFF) + + val greyInnerShadowColor2 = Color(0x1A007AFF) + // [END_EXCLUDE] + + val blueDropShadow by transition.animateColor( + label = "shadow_color", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) Color.Transparent else blueDropShadowColor + } + + // [START_EXCLUDE] + val darkBlueDropShadow by transition.animateColor( + label = "shadow_color", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) Color.Transparent else darkBlueDropShadowColor + } + + val innerShadowColor1 by transition.animateColor( + label = "shadow_color", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) greyInnerShadowColor1 + else greyInnerShadowColor2 + } + + val innerShadowColor2 by transition.animateColor( + label = "shadow_color", + transitionSpec = { buttonPressAnimation() } + ) { pressed -> + if (pressed) Color(0x4D007AFF) + else Color(0x1A007AFF) + } + // [END_EXCLUDE] + + Box( + Modifier + .clickable( + interactionSource, indication = null + ) { + // ** ...... **// + } + .width(300.dp) + .height(200.dp) + .align(Alignment.Center) + .dropShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 10.dp, + spread = 0.dp, + color = blueDropShadow, + offset = DpOffset(x = 0.dp, -(2).dp), + alpha = shadowAlpha + ) + ) + .dropShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 10.dp, + spread = 0.dp, + color = darkBlueDropShadow, + offset = DpOffset(x = 2.dp, 6.dp), + alpha = shadowAlpha + ) + ) + // note that the background needs to be defined before defining the inner shadow + .background( + color = Color(0xFFFFFFFF), + shape = RoundedCornerShape(70.dp) + ) + .innerShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 8.dp, + spread = 4.dp, + color = innerShadowColor2, + offset = DpOffset(x = 4.dp, 0.dp) + ) + ) + .innerShadow( + shape = RoundedCornerShape(70.dp), + shadow = Shadow( + radius = 20.dp, + spread = 4.dp, + color = innerShadowColor1, + offset = DpOffset(x = 4.dp, 0.dp), + alpha = innerShadowAlpha + ) + ) + + ) { + Text( + "Animated Shadows", + // [START_EXCLUDE] + modifier = Modifier + .align(Alignment.Center), + style = MaterialTheme.typography.bodyLarge, + fontSize = 24.sp, + color = Color.Black + // [END_EXCLUDE] + ) + } + } + } +} +// [END android_compose_graphics_animated_shadow] + +@Preview( + showBackground = true, + backgroundColor = 0xFFFFCC00 +) +// [START android_compose_graphics_neobrutal_shadow] +@Composable +fun NeoBrutalShadows() { + SnippetsTheme { + val dropShadowColor = Color(0xFF007AFF) + val borderColor = Color(0xFFFF2D55) + Box(Modifier.fillMaxSize()) { + Box( + Modifier + .width(300.dp) + .height(200.dp) + .align(Alignment.Center) + .dropShadow( + shape = RoundedCornerShape(0.dp), + shadow = Shadow( + radius = 0.dp, + spread = 0.dp, + color = dropShadowColor, + offset = DpOffset(x = 8.dp, 8.dp) + ) + ) + .border( + 8.dp, borderColor + ) + .background( + color = Color.White, + shape = RoundedCornerShape(0.dp) + ) + ) { + Text( + "Neobrutal Shadows", + modifier = Modifier.align(Alignment.Center), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} +// [END android_compose_graphics_neobrutal_shadow] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt new file mode 100644 index 000000000..f254da288 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/ShapesSnippets.kt @@ -0,0 +1,601 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.graphics + +import android.graphics.PointF +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asComposePath +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.core.graphics.plus +import androidx.core.graphics.times +import androidx.graphics.shapes.CornerRounding +import androidx.graphics.shapes.Cubic +import androidx.graphics.shapes.Morph +import androidx.graphics.shapes.RoundedPolygon +import androidx.graphics.shapes.star +import androidx.graphics.shapes.toPath +import com.example.compose.snippets.R +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.max +import kotlin.math.sin + +@Preview +@Composable +fun BasicShapeCanvas() { + // [START android_compose_graphics_basic_polygon] + Box( + modifier = Modifier + .drawWithCache { + val roundedPolygon = RoundedPolygon( + numVertices = 6, + radius = size.minDimension / 2, + centerX = size.width / 2, + centerY = size.height / 2 + ) + val roundedPolygonPath = roundedPolygon.toPath().asComposePath() + onDrawBehind { + drawPath(roundedPolygonPath, color = Color.Blue) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_basic_polygon] +} + +@Preview +@Composable +private fun RoundedShapeExample() { + // [START android_compose_graphics_polygon_rounding] + Box( + modifier = Modifier + .drawWithCache { + val roundedPolygon = RoundedPolygon( + numVertices = 3, + radius = size.minDimension / 2, + centerX = size.width / 2, + centerY = size.height / 2, + rounding = CornerRounding( + size.minDimension / 10f, + smoothing = 1f + ) + ) + val roundedPolygonPath = roundedPolygon.toPath().asComposePath() + onDrawBehind { + drawPath(roundedPolygonPath, color = Color.Black) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_polygon_rounding] +} + +@Preview +@Composable +private fun RoundedShapeSmoothnessExample() { + // [START android_compose_graphics_polygon_rounding_smooth] + Box( + modifier = Modifier + .drawWithCache { + val roundedPolygon = RoundedPolygon( + numVertices = 3, + radius = size.minDimension / 2, + centerX = size.width / 2, + centerY = size.height / 2, + rounding = CornerRounding( + size.minDimension / 10f, + smoothing = 0.1f + ) + ) + val roundedPolygonPath = roundedPolygon.toPath().asComposePath() + onDrawBehind { + drawPath(roundedPolygonPath, color = Color.Black) + } + } + .size(100.dp) + ) + + // [END android_compose_graphics_polygon_rounding_smooth] +} + +@Preview +@Composable +private fun MorphExample() { + // [START android_compose_graphics_polygon_morph] + Box( + modifier = Modifier + .drawWithCache { + val triangle = RoundedPolygon( + numVertices = 3, + radius = size.minDimension / 2f, + centerX = size.width / 2f, + centerY = size.height / 2f, + rounding = CornerRounding( + size.minDimension / 10f, + smoothing = 0.1f + ) + ) + val square = RoundedPolygon( + numVertices = 4, + radius = size.minDimension / 2f, + centerX = size.width / 2f, + centerY = size.height / 2f + ) + + val morph = Morph(start = triangle, end = square) + val morphPath = morph + .toPath(progress = 0.5f).asComposePath() + + onDrawBehind { + drawPath(morphPath, color = Color.Black) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_polygon_morph] +} + +@Preview +@Composable +private fun MorphExampleAnimation() { + // [START android_compose_graphics_polygon_morph_animation] + val infiniteAnimation = rememberInfiniteTransition(label = "infinite animation") + val morphProgress = infiniteAnimation.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + tween(500), + repeatMode = RepeatMode.Reverse + ), + label = "morph" + ) + Box( + modifier = Modifier + .drawWithCache { + val triangle = RoundedPolygon( + numVertices = 3, + radius = size.minDimension / 2f, + centerX = size.width / 2f, + centerY = size.height / 2f, + rounding = CornerRounding( + size.minDimension / 10f, + smoothing = 0.1f + ) + ) + val square = RoundedPolygon( + numVertices = 4, + radius = size.minDimension / 2f, + centerX = size.width / 2f, + centerY = size.height / 2f + ) + + val morph = Morph(start = triangle, end = square) + val morphPath = morph + .toPath(progress = morphProgress.value) + .asComposePath() + + onDrawBehind { + drawPath(morphPath, color = Color.Black) + } + } + .fillMaxSize() + ) + // [END android_compose_graphics_polygon_morph_animation] +} + +// [START android_compose_morph_to_path] +/** + * Transforms the morph at a given progress into a [Path]. + * It can optionally be scaled, using the origin (0,0) as pivot point. + */ +fun Morph.toComposePath(progress: Float, scale: Float = 1f, path: Path = Path()): Path { + var first = true + path.rewind() + forEachCubic(progress) { bezier -> + if (first) { + path.moveTo(bezier.anchor0X * scale, bezier.anchor0Y * scale) + first = false + } + path.cubicTo( + bezier.control0X * scale, bezier.control0Y * scale, + bezier.control1X * scale, bezier.control1Y * scale, + bezier.anchor1X * scale, bezier.anchor1Y * scale + ) + } + path.close() + return path +} +// [END android_compose_morph_to_path] +/** + * Function used to create a Path from a list of Cubics. + */ +// [START android_compose_list_cubics_to_path] +fun List.toPath(path: Path = Path(), scale: Float = 1f): Path { + path.rewind() + firstOrNull()?.let { first -> + path.moveTo(first.anchor0X * scale, first.anchor0Y * scale) + } + for (bezier in this) { + path.cubicTo( + bezier.control0X * scale, bezier.control0Y * scale, + bezier.control1X * scale, bezier.control1Y * scale, + bezier.anchor1X * scale, bezier.anchor1Y * scale + ) + } + path.close() + return path +} +// [END android_compose_list_cubics_to_path] + +// [START android_compose_morph_clip_shape] +class MorphPolygonShape( + private val morph: Morph, + private val percentage: Float +) : Shape { + + private val matrix = Matrix() + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f + // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y. + matrix.scale(size.width / 2f, size.height / 2f) + matrix.translate(1f, 1f) + + val path = morph.toPath(progress = percentage).asComposePath() + path.transform(matrix) + return Outline.Generic(path) + } +} + +// [END android_compose_morph_clip_shape] +@Preview +@Composable +private fun MorphOnClick() { + // [START android_compose_graphics_morph_on_click] + val shapeA = remember { + RoundedPolygon( + 6, + rounding = CornerRounding(0.2f) + ) + } + val shapeB = remember { + RoundedPolygon.star( + 6, + rounding = CornerRounding(0.1f) + ) + } + val morph = remember { + Morph(shapeA, shapeB) + } + val interactionSource = remember { + MutableInteractionSource() + } + val isPressed by interactionSource.collectIsPressedAsState() + val animatedProgress = animateFloatAsState( + targetValue = if (isPressed) 1f else 0f, + label = "progress", + animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium) + ) + Box( + modifier = Modifier + .size(200.dp) + .padding(8.dp) + .clip(MorphPolygonShape(morph, animatedProgress.value)) + .background(Color(0xFF80DEEA)) + .size(200.dp) + .clickable(interactionSource = interactionSource, indication = null) { + } + ) { + Text("Hello", modifier = Modifier.align(Alignment.Center)) + } + // [END android_compose_graphics_morph_on_click] +} + +// [START android_compose_shapes_polygon_compose_shape] +fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) } +class RoundedPolygonShape( + private val polygon: RoundedPolygon, + private var matrix: Matrix = Matrix() +) : Shape { + private var path = Path() + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + path.rewind() + path = polygon.toPath().asComposePath() + matrix.reset() + val bounds = polygon.getBounds() + val maxDimension = max(bounds.width, bounds.height) + matrix.scale(size.width / maxDimension, size.height / maxDimension) + matrix.translate(-bounds.left, -bounds.top) + + path.transform(matrix) + return Outline.Generic(path) + } +} +// [END android_compose_shapes_polygon_compose_shape] + +@Preview +@Composable +fun ApplyPolygonAsClipBasic() { + // [START android_compose_shapes_apply_as_clip] + val hexagon = remember { + RoundedPolygon( + 6, + rounding = CornerRounding(0.2f) + ) + } + val clip = remember(hexagon) { + RoundedPolygonShape(polygon = hexagon) + } + Box( + modifier = Modifier + .clip(clip) + .background(MaterialTheme.colorScheme.secondary) + .size(200.dp) + ) { + Text( + "Hello Compose", + color = MaterialTheme.colorScheme.onSecondary, + modifier = Modifier.align(Alignment.Center) + ) + } + // [END android_compose_shapes_apply_as_clip] +} + +@Preview +@Composable +fun ApplyPolygonAsClipImage() { + // [START android_compose_shapes_apply_as_clip_advanced] + val hexagon = remember { + RoundedPolygon( + 6, + rounding = CornerRounding(0.2f) + ) + } + val clip = remember(hexagon) { + RoundedPolygonShape(polygon = hexagon) + } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = "Dog", + contentScale = ContentScale.Crop, + modifier = Modifier + .graphicsLayer { + this.shadowElevation = 6.dp.toPx() + this.shape = clip + this.clip = true + this.ambientShadowColor = Color.Black + this.spotShadowColor = Color.Black + } + .size(200.dp) + + ) + } + // [END android_compose_shapes_apply_as_clip_advanced] +} + +// [START android_compose_shapes_custom_rotating_morph_shape] +class CustomRotatingMorphShape( + private val morph: Morph, + private val percentage: Float, + private val rotation: Float +) : Shape { + + private val matrix = Matrix() + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f + // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y. + matrix.scale(size.width / 2f, size.height / 2f) + matrix.translate(1f, 1f) + matrix.rotateZ(rotation) + + val path = morph.toPath(progress = percentage).asComposePath() + path.transform(matrix) + + return Outline.Generic(path) + } +} + +@Preview +@Composable +private fun RotatingScallopedProfilePic() { + val shapeA = remember { + RoundedPolygon( + 12, + rounding = CornerRounding(0.2f) + ) + } + val shapeB = remember { + RoundedPolygon.star( + 12, + rounding = CornerRounding(0.2f) + ) + } + val morph = remember { + Morph(shapeA, shapeB) + } + val infiniteTransition = rememberInfiniteTransition("infinite outline movement") + val animatedProgress = infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + tween(2000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "animatedMorphProgress" + ) + val animatedRotation = infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + tween(6000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "animatedMorphProgress" + ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = "Dog", + contentScale = ContentScale.Crop, + modifier = Modifier + .clip( + CustomRotatingMorphShape( + morph, + animatedProgress.value, + animatedRotation.value + ) + ) + .size(200.dp) + ) + } +} +// [END android_compose_shapes_custom_rotating_morph_shape] + +@Preview +@Composable +private fun CartesianPoints() { + // [START android_compose_shapes_custom_vertices] + val vertices = remember { + val radius = 1f + val radiusSides = 0.8f + val innerRadius = .1f + floatArrayOf( + radialToCartesian(radiusSides, 0f.toRadians()).x, + radialToCartesian(radiusSides, 0f.toRadians()).y, + radialToCartesian(radius, 90f.toRadians()).x, + radialToCartesian(radius, 90f.toRadians()).y, + radialToCartesian(radiusSides, 180f.toRadians()).x, + radialToCartesian(radiusSides, 180f.toRadians()).y, + radialToCartesian(radius, 250f.toRadians()).x, + radialToCartesian(radius, 250f.toRadians()).y, + radialToCartesian(innerRadius, 270f.toRadians()).x, + radialToCartesian(innerRadius, 270f.toRadians()).y, + radialToCartesian(radius, 290f.toRadians()).x, + radialToCartesian(radius, 290f.toRadians()).y, + ) + } + // [END android_compose_shapes_custom_vertices] + + // [START android_compose_shapes_custom_vertices_draw] + val rounding = remember { + val roundingNormal = 0.6f + val roundingNone = 0f + listOf( + CornerRounding(roundingNormal), + CornerRounding(roundingNone), + CornerRounding(roundingNormal), + CornerRounding(roundingNormal), + CornerRounding(roundingNone), + CornerRounding(roundingNormal), + ) + } + + val polygon = remember(vertices, rounding) { + RoundedPolygon( + vertices = vertices, + perVertexRounding = rounding + ) + } + Box( + modifier = Modifier + .drawWithCache { + val roundedPolygonPath = polygon.toPath().asComposePath() + onDrawBehind { + scale(size.width * 0.5f, size.width * 0.5f) { + translate(size.width * 0.5f, size.height * 0.5f) { + drawPath(roundedPolygonPath, color = Color(0xFFF15087)) + } + } + } + } + .size(400.dp) + ) + // [END android_compose_shapes_custom_vertices_draw] +} + +// [START android_compose_shapes_radial_to_cartesian] +internal fun Float.toRadians() = this * PI.toFloat() / 180f + +internal val PointZero = PointF(0f, 0f) +internal fun radialToCartesian( + radius: Float, + angleRadians: Float, + center: PointF = PointZero +) = directionVectorPointF(angleRadians) * radius + center + +internal fun directionVectorPointF(angleRadians: Float) = + PointF(cos(angleRadians), sin(angleRadians)) +// [END android_compose_shapes_radial_to_cartesian] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/images/AnimateImageSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/images/AnimateImageSnippets.kt new file mode 100644 index 000000000..2eab0220d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/images/AnimateImageSnippets.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.images + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp + +// [START android_compose_images_imageresizeonscrollexample] +@Composable +fun ImageResizeOnScrollExample( + modifier: Modifier = Modifier, + maxImageSize: Dp = 300.dp, + minImageSize: Dp = 100.dp +) { + var currentImageSize by remember { mutableStateOf(maxImageSize) } + var imageScale by remember { mutableFloatStateOf(1f) } + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + // Calculate the change in image size based on scroll delta + val delta = available.y + val newImageSize = currentImageSize + delta.dp + val previousImageSize = currentImageSize + + // Constrain the image size within the allowed bounds + currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize) + val consumed = currentImageSize - previousImageSize + + // Calculate the scale for the image + imageScale = currentImageSize / maxImageSize + + // Return the consumed scroll amount + return Offset(0f, consumed.value) + } + } + } + + Box(Modifier.nestedScroll(nestedScrollConnection)) { + LazyColumn( + Modifier + .fillMaxWidth() + .padding(15.dp) + .offset { + IntOffset(0, currentImageSize.roundToPx()) + } + ) { + // Placeholder list items + items(100, key = { it }) { + Text( + text = "Item: $it", + style = MaterialTheme.typography.bodyLarge + ) + } + } + + Image( + painter = ColorPainter(Color.Red), + contentDescription = "Red color image", + Modifier + .size(maxImageSize) + .align(Alignment.TopCenter) + .graphicsLayer { + scaleX = imageScale + scaleY = imageScale + // Center the image vertically as it scales + translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f + } + ) + } +} +// [END android_compose_images_imageresizeonscrollexample] + +@Preview(showBackground = true) +@Composable +private fun ImageSizeOnScrollScreenPreview() { + ImageResizeOnScrollExample() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/images/CustomPainterSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/images/CustomPainterSnippets.kt new file mode 100644 index 000000000..b503ba123 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/images/CustomPainterSnippets.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.images + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.paint +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize +import com.example.compose.snippets.R +import kotlin.math.roundToInt + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +@Preview +@Composable +fun CustomPainterUsage() { + // [START android_compose_images_custom_painter_usage] + val rainbowImage = ImageBitmap.imageResource(id = R.drawable.rainbow) + val dogImage = ImageBitmap.imageResource(id = R.drawable.dog) + val customPainter = remember { + OverlayImagePainter(dogImage, rainbowImage) + } + Image( + painter = customPainter, + contentDescription = stringResource(id = R.string.dog_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier.wrapContentSize() + ) + // [END android_compose_images_custom_painter_usage] +} + +// [START android_compose_images_custom_painter] +class OverlayImagePainter constructor( + private val image: ImageBitmap, + private val imageOverlay: ImageBitmap, + private val srcOffset: IntOffset = IntOffset.Zero, + private val srcSize: IntSize = IntSize(image.width, image.height), + private val overlaySize: IntSize = IntSize(imageOverlay.width, imageOverlay.height) +) : Painter() { + + private val size: IntSize = validateSize(srcOffset, srcSize) + override fun DrawScope.onDraw() { + // draw the first image without any blend mode + drawImage( + image, + srcOffset, + srcSize, + dstSize = IntSize( + this@onDraw.size.width.roundToInt(), + this@onDraw.size.height.roundToInt() + ) + ) + // draw the second image with an Overlay blend mode to blend the two together + drawImage( + imageOverlay, + srcOffset, + overlaySize, + dstSize = IntSize( + this@onDraw.size.width.roundToInt(), + this@onDraw.size.height.roundToInt() + ), + blendMode = BlendMode.Overlay + ) + } + + /** + * Return the dimension of the underlying [ImageBitmap] as it's intrinsic width and height + */ + override val intrinsicSize: Size get() = size.toSize() + + private fun validateSize(srcOffset: IntOffset, srcSize: IntSize): IntSize { + require( + srcOffset.x >= 0 && + srcOffset.y >= 0 && + srcSize.width >= 0 && + srcSize.height >= 0 && + srcSize.width <= image.width && + srcSize.height <= image.height + ) + return srcSize + } +} +// [END android_compose_images_custom_painter] + +@Preview +@Composable +fun CustomPainterModifier() { + // [START android_compose_custom_painter_modifier] + val rainbowImage = ImageBitmap.imageResource(id = R.drawable.rainbow) + val dogImage = ImageBitmap.imageResource(id = R.drawable.dog) + val customPainter = remember { + OverlayImagePainter(dogImage, rainbowImage) + } + Box( + modifier = + Modifier.background(color = Color.Gray) + .padding(30.dp) + .background(color = Color.Yellow) + .paint(customPainter) + ) { /** intentionally empty **/ } + // [END android_compose_custom_painter_modifier] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/images/CustomizeImageSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/images/CustomizeImageSnippets.kt new file mode 100644 index 000000000..d663aa7af --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/images/CustomizeImageSnippets.kt @@ -0,0 +1,359 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.images + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +@Preview +@Composable +fun ImageExamplesScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + ContentScaleExample() + ClipImageExample() + ClipRoundedCorner() + CustomClippingShape() + ImageWithBorder() + ImageRainbowBorder() + ImageAspectRatio() + ImageColorFilter() + ImageBlendMode() + ImageColorMatrix() + ImageAdjustBrightnessContrast() + ImageInvertColors() + ImageBlur() + ImageBlurEdgeTreatment() + } +} + +@Preview +@Composable +fun ContentScaleExample() { + // [START android_compose_content_scale] + val imageModifier = Modifier + .size(150.dp) + .border(BorderStroke(1.dp, Color.Black)) + .background(Color.Yellow) + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + contentScale = ContentScale.Fit, + modifier = imageModifier + ) + // [END android_compose_content_scale] +} + +@Preview +@Composable +fun ClipImageExample() { + // [START android_compose_clip_image] + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(200.dp) + .clip(CircleShape) + ) + // [END android_compose_clip_image] +} + +@Preview +@Composable +fun ClipRoundedCorner() { + // [START android_compose_clip_image_rounded_corner] + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(200.dp) + .clip(RoundedCornerShape(16.dp)) + ) + // [END android_compose_clip_image_rounded_corner] +} + +@Preview +@Composable +fun CustomClippingShape() { + // [START android_compose_custom_clipping_shape] + class SquashedOval : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val path = Path().apply { + // We create an Oval that starts at ¼ of the width, and ends at ¾ of the width of the container. + addOval( + Rect( + left = size.width / 4f, + top = 0f, + right = size.width * 3 / 4f, + bottom = size.height + ) + ) + } + return Outline.Generic(path = path) + } + } + + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(200.dp) + .clip(SquashedOval()) + ) + // [END android_compose_custom_clipping_shape] +} + +@Preview +@Composable +fun ImageWithBorder() { + // [START android_compose_image_border] + val borderWidth = 4.dp + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(150.dp) + .border( + BorderStroke(borderWidth, Color.Yellow), + CircleShape + ) + .padding(borderWidth) + .clip(CircleShape) + ) + // [END android_compose_image_border] +} + +@Preview +@Composable +fun ImageRainbowBorder() { + // [START android_compose_image_rainbow_border] + val rainbowColorsBrush = remember { + Brush.sweepGradient( + listOf( + Color(0xFF9575CD), + Color(0xFFBA68C8), + Color(0xFFE57373), + Color(0xFFFFB74D), + Color(0xFFFFF176), + Color(0xFFAED581), + Color(0xFF4DD0E1), + Color(0xFF9575CD) + ) + ) + } + val borderWidth = 4.dp + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(150.dp) + .border( + BorderStroke(borderWidth, rainbowColorsBrush), + CircleShape + ) + .padding(borderWidth) + .clip(CircleShape) + ) + // [END android_compose_image_rainbow_border] +} + +@Composable +@Preview +fun ImageAspectRatio() { + // [START android_compose_image_aspect_ratio] + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + modifier = Modifier.aspectRatio(16f / 9f) + ) + // [END android_compose_image_aspect_ratio] +} + +@Composable +@Preview +fun ImageColorFilter() { + // [START android_compose_image_color_filter] + Image( + painter = painterResource(id = R.drawable.baseline_directions_bus_24), + contentDescription = stringResource(id = R.string.bus_content_description), + colorFilter = ColorFilter.tint(Color.Yellow) + ) + // [END android_compose_image_color_filter] +} + +@Preview +@Composable +fun ImageBlendMode() { + // [START android_compose_image_blend_mode] + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + colorFilter = ColorFilter.tint(Color.Green, blendMode = BlendMode.Darken) + ) + // [END android_compose_image_blend_mode] +} + +@Preview +@Composable +fun ImageColorMatrix() { + // [START android_compose_image_colormatrix] + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }) + ) + // [END android_compose_image_colormatrix] +} + +@Preview +@Composable +fun ImageAdjustBrightnessContrast() { + // [START android_compose_image_brightness] + val contrast = 2f // 0f..10f (1 should be default) + val brightness = -180f // -255f..255f (0 should be default) + val colorMatrix = floatArrayOf( + contrast, 0f, 0f, 0f, brightness, + 0f, contrast, 0f, 0f, brightness, + 0f, 0f, contrast, 0f, brightness, + 0f, 0f, 0f, 1f, 0f + ) + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + colorFilter = ColorFilter.colorMatrix(ColorMatrix(colorMatrix)) + ) + // [END android_compose_image_brightness] +} + +@Preview +@Composable +fun ImageInvertColors() { + // [START android_compose_image_invert_colors] + val colorMatrix = floatArrayOf( + -1f, 0f, 0f, 0f, 255f, + 0f, -1f, 0f, 0f, 255f, + 0f, 0f, -1f, 0f, 255f, + 0f, 0f, 0f, 1f, 0f + ) + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + colorFilter = ColorFilter.colorMatrix(ColorMatrix(colorMatrix)) + ) + // [END android_compose_image_invert_colors] +} + +@Preview +@Composable +fun ImageBlur() { + // [START android_compose_image_blur] + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(150.dp) + .blur( + radiusX = 10.dp, + radiusY = 10.dp, + edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(8.dp)) + ) + ) + // [END android_compose_image_blur] +} + +@Preview +@Composable +fun ImageBlurEdgeTreatment() { + // [START android_compose_image_blur_edge_treatment] + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(150.dp) + .blur( + radiusX = 10.dp, + radiusY = 10.dp, + edgeTreatment = BlurredEdgeTreatment.Unbounded + ) + .clip(RoundedCornerShape(8.dp)) + ) + // / [END android_compose_image_blur_edge_treatment] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/images/LoadingImagesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/images/LoadingImagesSnippets.kt new file mode 100644 index 000000000..879b2f9a9 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/images/LoadingImagesSnippets.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.images + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import coil.compose.AsyncImage +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.example.compose.snippets.R + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +// Snippets for https://developer.android.com/jetpack/compose/graphics/images/loading +@Preview +@Composable +fun LoadingImageFromDisk() { + // [START android_compose_images_load_disk] + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description) + ) + // [END android_compose_images_load_disk] +} + +@Preview +@Composable +fun LoadingImageFromInternetCoil() { + // [START android_compose_images_load_internet_coil] + AsyncImage( + model = "https://example.com/image.jpg", + contentDescription = "Translated description of what the image contains" + ) + // [END android_compose_images_load_internet_coil] +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Preview +@Composable +fun LoadingImageFromInternetGlide() { + // [START android_compose_images_load_internet_glide] + GlideImage( + model = "https://example.com/image.jpg", + contentDescription = "Translated description of what the image contains" + ) + // [END android_compose_images_load_internet_glide] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/images/MaterialIconsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/images/MaterialIconsSnippets.kt new file mode 100644 index 000000000..fff218c28 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/images/MaterialIconsSnippets.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.images + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ShoppingCart +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.example.compose.snippets.R + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +@Preview +@Composable +fun MaterialIconsSnippets() { + // [START android_compose_images_icon_resources] + Icon( + painter = painterResource(R.drawable.baseline_directions_bus_24), + contentDescription = stringResource(id = R.string.bus_content_description) + ) + // [END android_compose_images_icon_resources] + + // [START android_compose_images_icon_prebuilt] + Icon( + Icons.Rounded.ShoppingCart, + contentDescription = stringResource(id = R.string.shopping_cart_content_desc) + ) + // [END android_compose_images_icon_prebuilt] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/images/ParallaxEffect.kt b/compose/snippets/src/main/java/com/example/compose/snippets/images/ParallaxEffect.kt new file mode 100644 index 000000000..3f5fd2527 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/images/ParallaxEffect.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.images + +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +// [START android_compose_images_parallax] +@Composable +fun ParallaxEffect() { + fun Modifier.parallaxLayoutModifier(scrollState: ScrollState, rate: Int) = + layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + val height = if (rate > 0) scrollState.value / rate else scrollState.value + layout(placeable.width, placeable.height) { + placeable.place(0, height) + } + } + + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollState), + ) { + + Image( + painterResource(id = R.drawable.cupcake), + contentDescription = "Android logo", + contentScale = ContentScale.Fit, + // Reduce scrolling rate by half. + modifier = Modifier.parallaxLayoutModifier(scrollState, 2) + ) + + Text( + text = stringResource(R.string.detail_placeholder), + modifier = Modifier + .background(Color.White) + .padding(horizontal = 8.dp), + + ) + } +} +// [END android_compose_images_parallax] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/images/VectorVsBitmapSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/images/VectorVsBitmapSnippets.kt new file mode 100644 index 000000000..a170f6ad0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/images/VectorVsBitmapSnippets.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.images + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import com.example.compose.snippets.R + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +@Preview +@Composable +fun ImageBitmapSnippets() { + // [START android_compose_images_bitmap_load] + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = stringResource(id = R.string.dog_content_description) + ) + // [END android_compose_images_bitmap_load] + + // [START android_compose_images_bitmap_simple] + val imageBitmap = ImageBitmap.imageResource(R.drawable.dog) + // [END android_compose_images_bitmap_simple] +} + +@Preview +@Composable +fun ImageVectorSnippet() { + // [START android_compose_images_vector_load] + Image( + painter = painterResource(id = R.drawable.baseline_shopping_cart_24), + contentDescription = stringResource(id = R.string.shopping_cart_content_desc) + ) + // [END android_compose_images_vector_load] + + // [START android_compose_images_vector_simple] + val imageVector = ImageVector.vectorResource(id = R.drawable.baseline_shopping_cart_24) + // [END android_compose_images_vector_simple] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeExistingArchSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeExistingArchSnippets.kt new file mode 100644 index 000000000..7ecbe1ef0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeExistingArchSnippets.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.example.compose.snippets.interop + +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import android.widget.LinearLayout +import android.widget.TextView +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController + +// [START android_compose_interop_existing_arch_viewmodels_in_compose] +class GreetingActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MaterialTheme { + Column { + GreetingScreen("user1") + GreetingScreen("user2") + } + } + } + } +} + +@Composable +fun GreetingScreen( + userId: String, + viewModel: GreetingViewModel = viewModel( + factory = GreetingViewModelFactory(userId) + ) +) { + val messageUser by viewModel.message.observeAsState("") + Text(messageUser) +} + +class GreetingViewModel(private val userId: String) : ViewModel() { + private val _message = MutableLiveData("Hi $userId") + val message: LiveData = _message +} + +class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return GreetingViewModel(userId) as T + } +} +// [END android_compose_interop_existing_arch_viewmodels_in_compose] + +// [START android_compose_interop_existing_arch_viewmodels_in_compose_nav_graph] +@Composable +fun MyApp() { + NavHost(rememberNavController(), startDestination = "profile/{userId}") { + /* ... */ + composable("profile/{userId}") { backStackEntry -> + GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") + } + } +} +// [END android_compose_interop_existing_arch_viewmodels_in_compose_nav_graph] + +// [START android_compose_interop_existing_arch_views_sot] +class CustomViewGroup @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : LinearLayout(context, attrs, defStyle) { + + // Source of truth in the View system as mutableStateOf + // to make it thread-safe for Compose + private var text by mutableStateOf("") + + private val textView: TextView + + init { + orientation = VERTICAL + + textView = TextView(context) + val composeView = ComposeView(context).apply { + setContent { + MaterialTheme { + TextField(value = text, onValueChange = { updateState(it) }) + } + } + } + + addView(textView) + addView(composeView) + } + + // Update both the source of truth and the TextView + private fun updateState(newValue: String) { + text = newValue + textView.text = newValue + } +} +// [END android_compose_interop_existing_arch_views_sot] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeExistingUISnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeExistingUISnippets.kt new file mode 100644 index 000000000..33c601342 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeExistingUISnippets.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.interop + +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import androidx.activity.ComponentActivity +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.AbstractComposeView +import com.example.compose.snippets.R +import com.example.compose.snippets.databinding.ActivityExampleBinding + +// [START android_compose_interop_existing_ui_shared] +@Composable +fun CallToActionButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondary + ), + onClick = onClick, + modifier = modifier, + ) { + Text(text) + } +} + +class CallToActionViewButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : AbstractComposeView(context, attrs, defStyle) { + + var text by mutableStateOf("") + var onClick by mutableStateOf({}) + + @Composable + override fun Content() { + YourAppTheme { + CallToActionButton(text, onClick) + } + } +} +// [START_EXCLUDE silent] +@Composable +fun YourAppTheme(content: @Composable () -> Unit) { +} +// [END_EXCLUDE] +// [END android_compose_interop_existing_ui_shared] + +// [START android_compose_interop_existing_ui_shared_view_binding] +class ViewBindingActivity : ComponentActivity() { + + private lateinit var binding: ActivityExampleBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityExampleBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.callToAction.apply { + text = getString(R.string.greeting) + onClick = { /* Do something */ } + } + } +} +// [END android_compose_interop_existing_ui_shared_view_binding] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeWithOtherLibraries.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeWithOtherLibraries.kt new file mode 100644 index 000000000..0b3b7467c --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/ComposeWithOtherLibraries.kt @@ -0,0 +1,283 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.interop + +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import androidx.navigation.compose.rememberNavController +import androidx.paging.PagingData +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import coil.compose.rememberAsyncImagePainter +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.rememberCameraPositionState +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +// [START android_compose_libraries_activity_contract] +@Composable +fun GetContentExample() { + var imageUri by remember { mutableStateOf(null) } + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + imageUri = uri + } + Column { + Button(onClick = { launcher.launch("image/*") }) { + Text(text = "Load Image") + } + Image( + painter = rememberAsyncImagePainter(imageUri), + contentDescription = "My Image" + ) + } +} +// [END android_compose_libraries_activity_contract] + +@Composable +private fun BackButtonExample() { + // [START android_compose_libraries_back_button] + var backHandlingEnabled by remember { mutableStateOf(true) } + BackHandler(backHandlingEnabled) { + // Handle back press + } + // [END android_compose_libraries_back_button] +} + +private object ViewModelExamples { + // [START android_compose_libraries_viewmodel] + class MyViewModel : ViewModel() { /*...*/ } + + // import androidx.lifecycle.viewmodel.compose.viewModel + @Composable + fun MyScreen( + viewModel: MyViewModel = viewModel() + ) { + // use viewModel here + } + // [END android_compose_libraries_viewmodel] +} + +private object ViewModelExample2 { + // [START android_compose_libraries_viewmodel_2] + class MyViewModel : ViewModel() { /*...*/ } + // import androidx.lifecycle.viewmodel.compose.viewModel + @Composable + fun MyScreen( + // Returns the same instance as long as the activity is alive, + // just as if you grabbed the instance from an Activity or Fragment + viewModel: MyViewModel = viewModel() + ) { /* ... */ } + + @Composable + fun MyScreen2( + viewModel: MyViewModel = viewModel() // Same instance as in MyScreen + ) { /* ... */ } + // [END android_compose_libraries_viewmodel_2] +} + +private object StreamData { + + class MyViewModel : ViewModel() { + val exampleLiveData: LiveData = MutableLiveData("") + } + // [START android_compose_libraries_stream_of_data] + // import androidx.lifecycle.viewmodel.compose.viewModel + @Composable + fun MyScreen( + viewModel: MyViewModel = viewModel() + ) { + val dataExample = viewModel.exampleLiveData.observeAsState() + + // Because the state is read here, + // MyScreen recomposes whenever dataExample changes. + dataExample.value?.let { + ShowData(dataExample) + } + } + // [END android_compose_libraries_stream_of_data] + + @Composable + fun ShowData(value: State) { + } +} + +object HiltExample3 { + // [START android_compose_libraries_hilt_3] + @HiltViewModel + class MyViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val repository: ExampleRepository + ) : ViewModel() { /* ... */ } + + // import androidx.lifecycle.viewmodel.compose.viewModel + @Composable + fun MyScreen( + viewModel: MyViewModel = viewModel() + ) { /* ... */ } + + // [END android_compose_libraries_hilt_3] + interface ExampleRepository +} + +object HiltViewModel { + @HiltViewModel + class MyViewModel @Inject constructor() : ViewModel() { /* ... */ } + // [START android_compose_libraries_hilt_viewmodel] + // import androidx.hilt.navigation.compose.hiltViewModel + + @Composable + fun MyApp() { + val navController = rememberNavController() + val startRoute = "example" + NavHost(navController, startDestination = startRoute) { + composable("example") { backStackEntry -> + // Creates a ViewModel from the current BackStackEntry + // Available in the androidx.hilt:hilt-navigation-compose artifact + val viewModel = hiltViewModel() + MyScreen(viewModel) + } + /* ... */ + } + } + // [END android_compose_libraries_hilt_viewmodel] + + @Composable + private fun MyScreen(viewModel: MyViewModel) { + } +} + +object HiltViewModelBackStack { + @HiltViewModel + class MyViewModel @Inject constructor() : ViewModel() { /* ... */ } + + @HiltViewModel + class ParentViewModel @Inject constructor() : ViewModel() { /* ... */ } + // [START android_compose_libraries_hilt_viewmodel_back_stack] + // import androidx.hilt.navigation.compose.hiltViewModel + // import androidx.navigation.compose.getBackStackEntry + + @Composable + fun MyApp() { + val navController = rememberNavController() + val startRoute = "example" + val innerStartRoute = "exampleWithRoute" + NavHost(navController, startDestination = startRoute) { + navigation(startDestination = innerStartRoute, route = "Parent") { + // ... + composable("exampleWithRoute") { backStackEntry -> + val parentEntry = remember(backStackEntry) { + navController.getBackStackEntry("Parent") + } + val parentViewModel = hiltViewModel(parentEntry) + ExampleWithRouteScreen(parentViewModel) + } + } + } + } + // [END android_compose_libraries_hilt_viewmodel_back_stack] + + @Composable + private fun ExampleWithRouteScreen(viewModel: ParentViewModel) { + } +} + +private object PagingExample { + // [START android_compose_libraries_paging_example] + @Composable + fun MyScreen(flow: Flow>) { + val lazyPagingItems = flow.collectAsLazyPagingItems() + LazyColumn { + items( + lazyPagingItems.itemCount, + key = lazyPagingItems.itemKey { it } + ) { index -> + val item = lazyPagingItems[index] + Text("Item is $item") + } + } + } + // [END android_compose_libraries_paging_example] +} + +private object MapsExample { + // [START android_compose_libraries_maps_example] + @Composable + fun MapsExample() { + val singapore = LatLng(1.35, 103.87) + val cameraPositionState = rememberCameraPositionState { + position = CameraPosition.fromLatLngZoom(singapore, 10f) + } + GoogleMap( + modifier = Modifier.fillMaxSize(), + cameraPositionState = cameraPositionState + ) { + Marker( + state = remember { MarkerState(position = singapore) }, + title = "Singapore", + snippet = "Marker in Singapore" + ) + } + } + // [END android_compose_libraries_maps_example] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt new file mode 100644 index 000000000..efad40fd7 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/InteroperabilityAPIsSnippets.kt @@ -0,0 +1,375 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.interop + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Color +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.viewinterop.AndroidViewBinding +import androidx.fragment.app.Fragment +import com.example.compose.snippets.MyActivity +import com.example.compose.snippets.R +import com.example.compose.snippets.databinding.ExampleLayoutBinding +import com.example.compose.snippets.databinding.FragmentExampleBinding +import com.example.compose.snippets.databinding.MyFragmentLayoutBinding + +// [START android_compose_interop_apis_compose_in_views] +class ExampleActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { // In here, we can call composables! + MaterialTheme { + Greeting(name = "compose") + } + } + } +} + +@Composable +fun Greeting(name: String) { + Text(text = "Hello $name!") +} +// [END android_compose_interop_apis_compose_in_views] + +// [START android_compose_interop_apis_compose_in_fragment_xml] +class ExampleFragmentXml : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = inflater.inflate(R.layout.fragment_example, container, false) + val composeView = view.findViewById(R.id.compose_view) + composeView.apply { + // Dispose of the Composition when the view's LifecycleOwner + // is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + // In Compose world + MaterialTheme { + Text("Hello Compose!") + } + } + } + return view + } +} +// [END android_compose_interop_apis_compose_in_fragment_xml] + +// [START android_compose_interop_apis_compose_in_fragment_view_binding] +class ExampleFragment : Fragment() { + + private var _binding: FragmentExampleBinding? = null + + // This property is only valid between onCreateView and onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentExampleBinding.inflate(inflater, container, false) + val view = binding.root + binding.composeView.apply { + // Dispose of the Composition when the view's LifecycleOwner + // is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + // In Compose world + MaterialTheme { + Text("Hello Compose!") + } + } + } + return view + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} +// [END android_compose_interop_apis_compose_in_fragment_view_binding] + +// [START android_compose_interop_apis_compose_in_fragment_no_xml] +class ExampleFragmentNoXml : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + // Dispose of the Composition when the view's LifecycleOwner + // is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MaterialTheme { + // In Compose world + Text("Hello Compose!") + } + } + } + } +} +// [END android_compose_interop_apis_compose_in_fragment_no_xml] + +// [START android_compose_interop_apis_compose_in_fragment_multiple] +class ExampleFragmentMultipleComposeView : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = LinearLayout(requireContext()).apply { + addView( + ComposeView(requireContext()).apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + id = R.id.compose_view_x + // ... + } + ) + addView(TextView(requireContext())) + addView( + ComposeView(requireContext()).apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + id = R.id.compose_view_y + // ... + } + ) + } +} +// [END android_compose_interop_apis_compose_in_fragment_multiple] + +// [START android_compose_interop_apis_android_view_reuse] +@Composable +fun AndroidViewInLazyList() { + LazyColumn { + items(100) { index -> + AndroidView( + modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree + factory = { context -> + MyView(context) + }, + update = { view -> + view.selectedItem = index + }, + onReset = { view -> + view.clear() + } + ) + } + } +} +// [END android_compose_interop_apis_android_view_reuse] + +// [START android_compose_interop_apis_views_in_compose] +@Composable +fun CustomView() { + var selectedItem by remember { mutableIntStateOf(0) } + + // Adds view to Compose + AndroidView( + modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree + factory = { context -> + // Creates view + MyView(context).apply { + // Sets up listeners for View -> Compose communication + setOnClickListener { + selectedItem = 1 + } + } + }, + update = { view -> + // View's been inflated or state read in this block has been updated + // Add logic here if necessary + + // As selectedItem is read here, AndroidView will recompose + // whenever the state changes + // Example of Compose -> View communication + view.selectedItem = selectedItem + } + ) +} + +@Composable +fun ContentExample() { + Column(Modifier.fillMaxSize()) { + Text("Look at this CustomView!") + CustomView() + } +} + +// [START_EXCLUDE silent] +class MyView(context: Context) : View(context) { + var selectedItem: Int = 0 + + fun clear() { } +} +// [END_EXCLUDE silent] +// [END android_compose_interop_apis_views_in_compose] + +// [START android_compose_interop_apis_android_view_binding] +@Composable +fun AndroidViewBindingExample() { + AndroidViewBinding(ExampleLayoutBinding::inflate) { + exampleView.setBackgroundColor(Color.GRAY) + } +} +// [END android_compose_interop_apis_android_view_binding] + +// [START android_compose_interop_apis_fragments_in_compose] +@Composable +fun FragmentInComposeExample() { + AndroidViewBinding(MyFragmentLayoutBinding::inflate) { + val myFragment = fragmentContainerView.getFragment() + // ... + } +} + +// [START_EXCLUDE silent] +class MyFragment : Fragment() +// [END_EXCLUDE] +// [END android_compose_interop_apis_fragments_in_compose] + +// [START android_compose_interop_apis_composition_locals] +@Composable +fun ToastGreetingButton(greeting: String) { + val context = LocalContext.current + Button(onClick = { + Toast.makeText(context, greeting, Toast.LENGTH_SHORT).show() + }) { + Text("Greet") + } +} +// [END android_compose_interop_apis_composition_locals] + +// [START android_compose_interop_apis_other_interactions] +// [START_EXCLUDE silent] +data class DataExample(val title: String) + +val data = DataExample("Hi") + +// [END_EXCLUDE] +class OtherInteractionsActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // get data from savedInstanceState + setContent { + MaterialTheme { + ExampleComposable(data, onButtonClick = { + startActivity(Intent(this, MyActivity::class.java)) + }) + } + } + } +} + +@Composable +fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) { + Button(onClick = onButtonClick) { + Text(data.title) + } +} +// [END android_compose_interop_apis_other_interactions] + +// [START android_compose_interop_apis_broadcast_receivers] +@Composable +fun SystemBroadcastReceiver( + systemAction: String, + onSystemEvent: (intent: Intent?) -> Unit +) { + // Grab the current context in this part of the UI tree + val context = LocalContext.current + + // Safely use the latest onSystemEvent lambda passed to the function + val currentOnSystemEvent by rememberUpdatedState(onSystemEvent) + + // If either context or systemAction changes, unregister and register again + DisposableEffect(context, systemAction) { + val intentFilter = IntentFilter(systemAction) + val broadcast = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + currentOnSystemEvent(intent) + } + } + + context.registerReceiver(broadcast, intentFilter) + + // When the effect leaves the Composition, remove the callback + onDispose { + context.unregisterReceiver(broadcast) + } + } +} + +@Composable +fun HomeScreen() { + + SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus -> + val isCharging = /* Get from batteryStatus ... */ true + /* Do something if the device is charging */ + } + + /* Rest of the HomeScreen */ +} +// [END android_compose_interop_apis_broadcast_receivers] + +// [START android_compose_interop_apis_layout_preview_composable] +@Preview +@Composable +fun GreetingPreview() { + Greeting(name = "Android") +} +// [END android_compose_interop_apis_layout_preview_composable] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt new file mode 100644 index 000000000..fec60d697 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationCommonScenariosSnippets.kt @@ -0,0 +1,431 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.interop + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Image +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +class RVActivity : ComponentActivity() { + private lateinit var composeView: ComposeView + private lateinit var recyclerView: RecyclerView + private fun step2() { + // [START android_compose_interop_migration_common_scenarios_recyclerview_step2] + // recyclerView.layoutManager = LinearLayoutManager(context) + composeView.setContent { + LazyColumn(Modifier.fillMaxSize()) { + // We use a LazyColumn since the layout manager of the RecyclerView is a vertical LinearLayoutManager + } + } + // [END android_compose_interop_migration_common_scenarios_recyclerview_step2] + } + + private fun step4() { + // [START android_compose_interop_migration_common_scenarios_recyclerview_step4] + val data = listOf(/* ... */) + composeView.setContent { + LazyColumn(Modifier.fillMaxSize()) { + items(data) { + ListItem(it) + } + } + } + // [END android_compose_interop_migration_common_scenarios_recyclerview_step4] + } + + private fun commonUseCase1() { + // [START android_compose_interop_migration_common_scenarios_recyclerview_common_use_case_1] + val itemDecoration = DividerItemDecoration(recyclerView.context, LinearLayoutManager.VERTICAL) + recyclerView.addItemDecoration(itemDecoration) + // [END android_compose_interop_migration_common_scenarios_recyclerview_common_use_case_1] + } + + @Composable + fun CommonUseCase2(data: List) { + // [START android_compose_interop_migration_common_scenarios_recyclerview_common_use_case_2] + LazyColumn(Modifier.fillMaxSize()) { + itemsIndexed(data) { index, d -> + ListItem(d) + if (index != data.size - 1) { + HorizontalDivider() + } + } + } + // [END android_compose_interop_migration_common_scenarios_recyclerview_common_use_case_2] + } +} + +// [START android_compose_interop_migration_common_scenarios_recyclerview_step3] +@Composable +fun ListItem(data: MyData, modifier: Modifier = Modifier) { + Row(modifier.fillMaxWidth()) { + Text(text = data.name) + // … other composables required for displaying `data` + } +} +// [END android_compose_interop_migration_common_scenarios_recyclerview_step3] + +// [START android_compose_interop_migration_common_scenarios_navigation_step_2] +class SampleActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // setContentView(this, R.layout.activity_sample) + setContent { + SampleApp(/* ... */) + } + } +} +// [END android_compose_interop_migration_common_scenarios_navigation_step_2] + +// [START android_compose_interop_migration_common_scenarios_navigation_step_2_1] +@Serializable data object First +@Serializable data class Second(val id: String) +@Serializable data object Third + +// [END android_compose_interop_migration_common_scenarios_navigation_step_2_1] + +private object MigrationCommonScenariosNavigationStep3 { + // [START android_compose_interop_migration_common_scenarios_navigation_step_3] + @Composable + fun SampleApp() { + val navController = rememberNavController() + // ... + } + // [END android_compose_interop_migration_common_scenarios_navigation_step_3] +} + +private object MigrationCommonScenariosNavigationStep4 { + // [START android_compose_interop_migration_common_scenarios_navigation_step_4] + @Composable + fun SampleApp() { + val navController = rememberNavController() + + SampleNavHost(navController = navController) + } + + @Composable + fun SampleNavHost( + navController: NavHostController + ) { + NavHost(navController = navController, startDestination = First) { + // ... + } + } + // [END android_compose_interop_migration_common_scenarios_navigation_step_4] +} + +// [START android_compose_interop_migration_common_scenarios_navigation_step_5] +class FirstFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setContent { + // FirstScreen(...) EXTRACT FROM HERE + } + } + } +} + +@Composable +fun SampleNavHost( + navController: NavHostController +) { + NavHost(navController = navController, startDestination = First) { + composable { + FirstScreen(/* ... */) // EXTRACT TO HERE + } + composable { + SecondScreen(/* ... */) + } + // ... + } +} +// [END android_compose_interop_migration_common_scenarios_navigation_step_5] + +// [START android_compose_interop_migration_common_scenarios_navigation_step_6] +@Composable +fun FirstScreen( + // viewModel: FirstViewModel = viewModel(), + viewModel: FirstViewModel = hiltViewModel(), + onButtonClick: () -> Unit = {}, +) { + // ... +} +// [END android_compose_interop_migration_common_scenarios_navigation_step_6] + +private object MigrationCommonScenariosNavigationStep7 { + // [START android_compose_interop_migration_common_scenarios_navigation_step_7] + @Composable + fun SampleNavHost( + navController: NavHostController + ) { + NavHost(navController = navController, startDestination = First) { + composable { + FirstScreen( + onButtonClick = { + // findNavController().navigate(firstScreenToSecondScreenAction) + navController.navigate(Second(id = "ABC")) + } + ) + } + composable { backStackEntry -> + val secondRoute = backStackEntry.toRoute() + SecondScreen( + id = secondRoute.id, + onIconClick = { + // findNavController().navigate(secondScreenToThirdScreenAction) + navController.navigate(Third) + } + ) + } + // ... + } + } +// [END android_compose_interop_migration_common_scenarios_navigation_step_7] +} + +class CoordinatorLayoutActivity : ComponentActivity() { + + private lateinit var composeView: ComposeView + + private fun step2() { + // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_step2] + composeView.setContent { + Scaffold(Modifier.fillMaxSize()) { contentPadding -> + // Scaffold contents + // [START_EXCLUDE] + Box(Modifier.padding(contentPadding)) + // [END_EXCLUDE] + } + } + // [END android_compose_interop_migration_common_scenarios_coordinatorlayout_step2] + } + + private fun step3() { + // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_step3] + composeView.setContent { + Scaffold(Modifier.fillMaxSize()) { contentPadding -> + val pagerState = rememberPagerState { + 10 + } + HorizontalPager( + state = pagerState, + modifier = Modifier.padding(contentPadding) + ) { /* Page contents */ } + } + } + // [END android_compose_interop_migration_common_scenarios_coordinatorlayout_step3] + } + + @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) + private fun step4() { + // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_step4] + composeView.setContent { + Scaffold( + Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { + Text("My App") + } + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { /* Handle click */ } + ) { + Icon( + Icons.Filled.Add, + contentDescription = "Add Button" + ) + } + } + ) { contentPadding -> + val pagerState = rememberPagerState { + 10 + } + HorizontalPager( + state = pagerState, + modifier = Modifier.padding(contentPadding) + ) { /* Page contents */ } + } + } + // [END android_compose_interop_migration_common_scenarios_coordinatorlayout_step4] + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun CommonUseCaseToolbars() { + // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_toolbars] + // 1. Create the TopAppBarScrollBehavior + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + + Scaffold( + topBar = { + TopAppBar( + title = { + Text("My App") + }, + // 2. Provide scrollBehavior to TopAppBar + scrollBehavior = scrollBehavior + ) + }, + // 3. Connect the scrollBehavior.nestedScrollConnection to the Scaffold + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { contentPadding -> + /* Contents */ + // [START_EXCLUDE] + Box(Modifier.padding(contentPadding)) + // [END_EXCLUDE] + } + // [END android_compose_interop_migration_common_scenarios_coordinatorlayout_toolbars] + } + + @Composable + private fun CommonUseCaseDrawers() { + // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_drawers] + ModalNavigationDrawer( + drawerContent = { + ModalDrawerSheet { + Text("Drawer title", modifier = Modifier.padding(16.dp)) + HorizontalDivider() + NavigationDrawerItem( + label = { Text(text = "Drawer Item") }, + selected = false, + onClick = { /*TODO*/ } + ) + // ...other drawer items + } + } + ) { + Scaffold(Modifier.fillMaxSize()) { contentPadding -> + // Scaffold content + // [START_EXCLUDE] + Box(Modifier.padding(contentPadding)) + // [END_EXCLUDE] + } + } + // [END android_compose_interop_migration_common_scenarios_coordinatorlayout_drawers] + } + + @Composable + private fun CommonUseCaseSnackbars() { + // [START android_compose_interop_migration_common_scenarios_coordinatorlayout_snackbars] + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Show snackbar") }, + icon = { Icon(Icons.Filled.Image, contentDescription = "") }, + onClick = { + scope.launch { + snackbarHostState.showSnackbar("Snackbar") + } + } + ) + } + ) { contentPadding -> + // Screen content + // [START_EXCLUDE] + Box(Modifier.padding(contentPadding)) + // [END_EXCLUDE] + } + // [END android_compose_interop_migration_common_scenarios_coordinatorlayout_snackbars] + } +} + +/* +Fakes needed for snippets to build: + */ + +@Composable +fun SampleApp() { +} + +@Composable +fun SecondScreen( + id: String = "", + onIconClick: () -> Unit = {}, +) { +} + +class FirstViewModel : ViewModel() +data class MyData( + val name: String +) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationOtherConsiderationsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationOtherConsiderationsSnippets.kt new file mode 100644 index 000000000..609e7140c --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationOtherConsiderationsSnippets.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.interop + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imeNestedScroll +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat + +// [START android_compose_migrate_considerations_side_effect] +@Composable +fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { + val analytics: FirebaseAnalytics = remember { + FirebaseAnalytics() + } + + // On every successful composition, update FirebaseAnalytics with + // the userType from the current User, ensuring that future analytics + // events have this metadata attached + SideEffect { + analytics.setUserProperty("userType", user.userType) + } + return analytics +} +// [END android_compose_migrate_considerations_side_effect] + +// [START android_compose_migrate_considerations_window_insets] +class WindowInsetsExampleActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { + MaterialTheme { + MyScreen() + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun MyScreen() { + Box { + LazyColumn( + modifier = Modifier + .fillMaxSize() // fill the entire window + .imePadding() // padding for the bottom for the IME + .imeNestedScroll(), // scroll IME at the bottom + content = { } + ) + FloatingActionButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) // normal 16dp of padding for FABs + .navigationBarsPadding() // padding for navigation bar + .imePadding(), // padding for when IME appears + onClick = { } + ) { + Icon(imageVector = Icons.Filled.Add, contentDescription = "Add") + } + } +} +// [END android_compose_migrate_considerations_window_insets] + +// [START android_compose_migrate_considerations_conditional_logic] +@Composable +fun MyComposable(showCautionIcon: Boolean) { + if (showCautionIcon) { + CautionIcon(/* ... */) + } +} +// [START_EXCLUDE silent] +@Composable +fun CautionIcon() { } +// [END_EXCLUDE] +// [END android_compose_migrate_considerations_conditional_logic] + +// [START android_compose_migrate_considerations_promote_encapsulation] +@Composable +fun AScreen() { + var isEnabled by rememberSaveable { mutableStateOf(false) } + + Column { + ImageWithEnabledOverlay(isEnabled) + ControlPanelWithToggle( + isEnabled = isEnabled, + onEnabledChanged = { isEnabled = it } + ) + } +} +// [START_EXCLUDE silent] +@Composable +fun ImageWithEnabledOverlay(isEnabled: Boolean) { } +@Composable +fun ControlPanelWithToggle(isEnabled: Boolean, onEnabledChanged: (Boolean) -> Unit) { } +// [END_EXCLUDE] +// [END android_compose_migrate_considerations_promote_encapsulation] + +// STUB +class User(name: String = "") { + val userType = "STUB" +} + +class FirebaseAnalytics { + fun setUserProperty(key: String, value: String) { + // STUB + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationStrategySnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationStrategySnippets.kt new file mode 100644 index 000000000..438f5366e --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/interop/MigrationStrategySnippets.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.interop + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.stringResource +import androidx.fragment.app.Fragment +import com.example.compose.snippets.R + +// [START android_compose_interop_migration_strategy_fragment] +class NewFeatureFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + NewFeatureScreen() + } + } + } +} +// [END android_compose_interop_migration_strategy_fragment] + +// [START android_compose_interop_migration_strategy_simple_screen] +@Composable +fun SimpleScreen() { + Column(Modifier.fillMaxSize()) { + Text( + text = stringResource(R.string.title), + style = MaterialTheme.typography.headlineMedium + ) + Text( + text = stringResource(R.string.subtitle), + style = MaterialTheme.typography.headlineSmall + ) + Text( + text = stringResource(R.string.body), + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.weight(1f)) + Button(onClick = { /* Handle click */ }, Modifier.fillMaxWidth()) { + Text(text = stringResource(R.string.confirm)) + } + } +} +// [END android_compose_interop_migration_strategy_simple_screen] + +@Composable +private fun NewFeatureScreen() { } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/JavaSnippets.java b/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/JavaSnippets.java new file mode 100644 index 000000000..0b0c9ef29 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/JavaSnippets.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.kotlin; + +public class JavaSnippets { + + // [START android_compose_kotlin_java_default_arguments] + // We don't need to do this in Kotlin! + void drawSquare(int sideLength) { } + + void drawSquare(int sideLength, int thickness) { } + + void drawSquare(int sideLength, int thickness, Color edgeColor) { } + // [END android_compose_kotlin_java_default_arguments] + + void functionCall() { + // [START android_compose_kotlin_java_call] + drawSquare(30, 5, Color.Red); + // [END android_compose_kotlin_java_call] + } +} + +class Color { + public static Color Red; +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt new file mode 100644 index 000000000..447b37031 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/kotlin/KotlinSnippets.kt @@ -0,0 +1,374 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.kotlin + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.FocusRequester.Companion.createRefs +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.inset +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import kotlin.math.roundToInt +import kotlin.properties.ReadWriteProperty +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +// [START android_compose_kotlin_default_arguments] +fun drawSquare( + sideLength: Int, + thickness: Int = 2, + edgeColor: Color = Color.Black +) { +} +// [END android_compose_kotlin_default_arguments] + +fun functionCall() { + // [START android_compose_kotlin_function_call] + drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red) + // [END android_compose_kotlin_function_call] +} + +@Composable +fun FunctionCallCompose() { + // [START android_compose_kotlin_function_call_composable_default] + Text(text = "Hello, Android!") + // [END android_compose_kotlin_function_call_composable_default] + + // [START android_compose_kotlin_function_call_composable_named_parameters] + Text( + text = "Hello, Android!", + color = Color.Unspecified, + fontSize = TextUnit.Unspecified, + letterSpacing = TextUnit.Unspecified, + overflow = TextOverflow.Clip + ) + // [END android_compose_kotlin_function_call_composable_named_parameters] +} + +@Composable +fun HighOrderFunctions(myClickFunction: () -> Unit) { + // [START android_compose_kotlin_high_order_function] + Button( + // ... + onClick = myClickFunction + ) + // [START_EXCLUDE] + {} + // [END_EXCLUDE] + // [END android_compose_kotlin_high_order_function] + + // [START android_compose_kotlin_high_order_function_lambda] + Button( + // ... + onClick = { + // do something + // do something else + } + ) { /* ... */ } + // [END android_compose_kotlin_high_order_function_lambda] +} + +@Composable +fun TrailingLambdas() { + // [START android_compose_kotlin_trailing_lambda] + Column( + modifier = Modifier.padding(16.dp), + content = { + Text("Some text") + Text("Some more text") + Text("Last text") + } + ) + // [END android_compose_kotlin_trailing_lambda] + + // [START android_compose_kotlin_trailing_lambda_content] + Column(modifier = Modifier.padding(16.dp)) { + Text("Some text") + Text("Some more text") + Text("Last text") + } + // [END android_compose_kotlin_trailing_lambda_content] + + // [START android_compose_kotlin_one_parameter] + Column { + Text("Some text") + Text("Some more text") + Text("Last text") + } + // [END android_compose_kotlin_one_parameter] +} + +@Composable +fun ScopesAndReceivers() { + // [START android_compose_kotlin_row_scope] + Row { + Text( + text = "Hello world", + // This Text is inside a RowScope so it has access to + // Alignment.CenterVertically but not to + // Alignment.CenterHorizontally, which would be available + // in a ColumnScope. + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + // [END android_compose_kotlin_row_scope] + + // [START android_compose_kotlin_receiver_scope] + Box( + modifier = Modifier.drawBehind { + // This method accepts a lambda of type DrawScope.() -> Unit + // therefore in this lambda we can access properties and functions + // available from DrawScope, such as the `drawRectangle` function. + drawRect( + /*...*/ + /* [START_EXCLUDE] */color = Color.Red/* [END_EXCLUDE] */ + ) + } + ) + // [END android_compose_kotlin_receiver_scope] +} + +// [START android_compose_kotlin_delegating_class] +class DelegatingClass { + var name: String by nameGetterFunction() + + // [START_EXCLUDE] + private fun nameGetterFunction(): ReadWriteProperty { + TODO("Not yet implemented") + } + // [END_EXCLUDE] +} +// [END android_compose_kotlin_delegating_class] + +fun delegateAccess() { + // [START android_compose_kotlin_delegate_access] + val myDC = DelegatingClass() + println("The name property is: " + myDC.name) + // [END android_compose_kotlin_delegate_access] +} + +@Composable +fun DelegatedProperties() { + // [START android_compose_kotlin_delegated_properties] + var showDialog by remember { mutableStateOf(false) } + + // Updating the var automatically triggers a state change + showDialog = true + // [END android_compose_kotlin_delegated_properties] +} + +// [START android_compose_kotlin_data_class] +data class Person(val name: String, val age: Int) +// [END android_compose_kotlin_data_class] + +fun destructuring() { + // [START android_compose_kotlin_destructuring] + val mary = Person(name = "Mary", age = 35) + + // ... + + val (name, age) = mary + // [END android_compose_kotlin_destructuring] +} + +@Composable +fun DestructuringCompose() { + // [START android_compose_kotlin_destructuring_compose] + Row { + + val (image, title, subtitle) = createRefs() + + // The `createRefs` function returns a data object; + // the first three components are extracted into the + // image, title, and subtitle variables. + + // ... + } + // [END android_compose_kotlin_destructuring_compose] +} + +data class Message(val message: Message?) + +// [START android_compose_kotlin_dsl] +@Composable +fun MessageList(messages: List) { + LazyColumn { + // Add a single item as a header + item { + Text("Message List") + } + + // Add list of messages + items(messages) { message -> + Message(message) + } + } +} +// [END android_compose_kotlin_dsl] + +@Composable +fun FunctionLiterals() { + // [START android_compose_kotlin_receiver] + Canvas(Modifier.size(120.dp)) { + // Draw grey background, drawRect function is provided by the receiver + drawRect(color = Color.Gray) + + // Inset content by 10 pixels on the left/right sides + // and 12 by the top/bottom + inset(10.0f, 12.0f) { + val quadrantSize = size / 2.0f + + // Draw a rectangle within the inset bounds + drawRect( + size = quadrantSize, + color = Color.Red + ) + + rotate(45.0f) { + drawRect(size = quadrantSize, color = Color.Blue) + } + } + } + // [END android_compose_kotlin_receiver] +} + +class MyViewModel : ViewModel() { + fun loadData() {} +} + +@Composable +fun Coroutines(scrollState: ScrollState, viewModel: MyViewModel) { + // [START android_compose_kotlin_coroutines] + // Create a CoroutineScope that follows this composable's lifecycle + val composableScope = rememberCoroutineScope() + Button( + // ... + onClick = { + // Create a new coroutine that scrolls to the top of the list + // and call the ViewModel to load data + composableScope.launch { + scrollState.animateScrollTo(0) // This is a suspend function + viewModel.loadData() + } + } + ) { /* ... */ } + // [END android_compose_kotlin_coroutines] +} + +@Composable +fun CoroutinesConcurrent(scrollState: ScrollState, viewModel: MyViewModel) { + // [START android_compose_kotlin_coroutines_concurrent] + // Create a CoroutineScope that follows this composable's lifecycle + val composableScope = rememberCoroutineScope() + Button( // ... + onClick = { + // Scroll to the top and load data in parallel by creating a new + // coroutine per independent work to do + composableScope.launch { + scrollState.animateScrollTo(0) + } + composableScope.launch { + viewModel.loadData() + } + } + ) { /* ... */ } + // [END android_compose_kotlin_coroutines_concurrent] +} + +// [START android_compose_kotlin_coroutines_animate] +@Composable +fun MoveBoxWhereTapped() { + // Creates an `Animatable` to animate Offset and `remember` it. + val animatedOffset = remember { + Animatable(Offset(0f, 0f), Offset.VectorConverter) + } + + Box( + // The pointerInput modifier takes a suspend block of code + Modifier + .fillMaxSize() + .pointerInput(Unit) { + // Create a new CoroutineScope to be able to create new + // coroutines inside a suspend function + coroutineScope { + while (true) { + // Wait for the user to tap on the screen and animate + // in the same block + awaitPointerEventScope { + val offset = awaitFirstDown().position + + // Launch a new coroutine to asynchronously animate to + // where the user tapped on the screen + launch { + // Animate to the pressed position + animatedOffset.animateTo(offset) + } + } + } + } + } + ) { + Text("Tap anywhere", Modifier.align(Alignment.Center)) + Box( + Modifier + .offset { + // Use the animated offset as the offset of this Box + IntOffset( + animatedOffset.value.x.roundToInt(), + animatedOffset.value.y.roundToInt() + ) + } + .size(40.dp) + .background(Color(0xff3c1361), CircleShape) + ) + } + // [END android_compose_kotlin_coroutines_animate] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt b/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt new file mode 100644 index 000000000..6e6c84019 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.landing + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.navigation.Destination + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LandingScreen( + navigate: (Destination) -> Unit +) { + Scaffold( + topBar = { + TopAppBar(title = { + Text(text = "Android snippets",) + }) + } + ) { padding -> + NavigationItems(modifier = Modifier.padding(padding)) { navigate(it) } + } +} + +@Composable +fun NavigationItems( + modifier: Modifier = Modifier, + navigate: (Destination) -> Unit +) { + LazyColumn( + modifier = modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + items(Destination.entries) { destination -> + NavigationItem(destination) { + navigate( + destination + ) + } + } + } +} + +@Composable +fun NavigationItem(destination: Destination, onClick: () -> Unit) { + ListItem( + headlineContent = { + Text(destination.title) + }, + modifier = Modifier + .heightIn(min = 48.dp) + .clickable { + onClick() + } + ) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt new file mode 100644 index 000000000..9447582cf --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.layouts + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowSizeClass + +/* +* Copyright 2023 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +// [START android_compose_adaptive_layouts_basic] +@Composable +fun MyApp( + windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass +) { + // Decide whether to show the top app bar based on window size class. + val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND) + + // MyScreen logic is based on the showTopAppBar boolean flag. + MyScreen( + showTopAppBar = showTopAppBar, + /* ... */ + ) +} +// [END android_compose_adaptive_layouts_basic] +@Composable +fun MyScreen(showTopAppBar: Boolean) { + // your content here +} + +// [START android_compose_layouts_adaptive_pane] +@Composable +fun AdaptivePane( + showOnePane: Boolean, + /* ... */ +) { + if (showOnePane) { + OnePane(/* ... */) + } else { + TwoPane(/* ... */) + } +} +// [END android_compose_layouts_adaptive_pane] + +@Composable +private fun WindowSizeClassSnippet() { + // [START android_compose_windowsizeclass] + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + // [END android_compose_windowsizeclass] +} + +@Composable +fun OnePane() { + // your content here +} +@Composable +fun TwoPane() { + // your content here +} +private object ManualMeasurementsSnippets { + // [START android_compose_layouts_manual_measurements] + @Composable + fun Card(/* ... */) { + BoxWithConstraints { + if (maxWidth < 400.dp) { + Column { + Image(/* ... */) + Title(/* ... */) + } + } else { + Row { + Column { + Title(/* ... */) + Description(/* ... */) + } + Image(/* ... */) + } + } + } + } + // [END android_compose_layouts_manual_measurements] + @Composable + private fun Image() { + } + @Composable + private fun Description() { + } + @Composable + private fun Title() { + } +} + +private object ManualMeasurementSnippets2 { + // [START android_compose_layouts_manual_measurements_2] + @Composable + fun Card( + imageUrl: String, + title: String, + description: String + ) { + BoxWithConstraints { + if (maxWidth < 400.dp) { + Column { + Image(imageUrl) + Title(title) + } + } else { + Row { + Column { + Title(title) + Description(description) + } + Image(imageUrl) + } + } + } + } + // [END android_compose_layouts_manual_measurements_2] + @Composable + private fun Image(imageUrl: String) { + } + @Composable + private fun Description(description: String) { + } + @Composable + private fun Title(title: String) { + } +} + +private object ManualMeasurementSnippets3 { + // [START android_compose_layouts_manual_measurements_3] + @Composable + fun Card( + imageUrl: String, + title: String, + description: String + ) { + var showMore by remember { mutableStateOf(false) } + + BoxWithConstraints { + if (maxWidth < 400.dp) { + Column { + Image(imageUrl) + Title(title) + } + } else { + Row { + Column { + Title(title) + Description( + description = description, + showMore = showMore, + onShowMoreToggled = { newValue -> + showMore = newValue + } + ) + } + Image(imageUrl) + } + } + } + } + // [END android_compose_layouts_manual_measurements_3] + @Composable + private fun Image(imageUrl: String) { + } + @Composable + private fun Description( + description: String, + showMore: Boolean, + onShowMoreToggled: (Boolean) -> Unit + ) { + } + @Composable + private fun Title(title: String) { + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AlignmentLinesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AlignmentLinesSnippets.kt new file mode 100644 index 000000000..8946384b1 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AlignmentLinesSnippets.kt @@ -0,0 +1,243 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.layouts + +/* +* Copyright 2023 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.AlignmentLine +import androidx.compose.ui.layout.FirstBaseline +import androidx.compose.ui.layout.HorizontalAlignmentLine +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +// [START android_compose_alignment_lines_basic] +fun Modifier.firstBaselineToTop( + firstBaselineToTop: Dp, +) = layout { measurable, constraints -> + // Measure the composable + val placeable = measurable.measure(constraints) + + // Check the composable has a first baseline + check(placeable[FirstBaseline] != AlignmentLine.Unspecified) + val firstBaseline = placeable[FirstBaseline] + + // Height of the composable with padding - first baseline + val placeableY = firstBaselineToTop.roundToPx() - firstBaseline + val height = placeable.height + placeableY + layout(placeable.width, height) { + // Where the composable gets placed + placeable.placeRelative(0, placeableY) + } +} + +@Preview +@Composable +private fun TextWithPaddingToBaseline() { + MaterialTheme { + Text("Hi there!", Modifier.firstBaselineToTop(32.dp)) + } +} +// [END android_compose_alignment_lines_basic] + +// [START android_compose_alignment_lines_example_max] +/** + * AlignmentLine defined by the maximum data value in a [BarChart] + */ +private val MaxChartValue = HorizontalAlignmentLine(merger = { old, new -> + min(old, new) +}) + +/** + * AlignmentLine defined by the minimum data value in a [BarChart] + */ +private val MinChartValue = HorizontalAlignmentLine(merger = { old, new -> + max(old, new) +}) +// [END android_compose_alignment_lines_example_max] + +// [START android_compose_alignment_lines_example_bar_chart] +@Composable +private fun BarChart( + dataPoints: List, + modifier: Modifier = Modifier, +) { + val maxValue: Float = remember(dataPoints) { dataPoints.maxOrNull()!! * 1.2f } + + BoxWithConstraints(modifier = modifier) { + val density = LocalDensity.current + with(density) { + // [START_EXCLUDE] + val yPositionRatio = remember(density, maxHeight, maxValue) { + maxHeight.toPx() / maxValue + } + val xPositionRatio = remember(density, maxWidth, dataPoints) { + maxWidth.toPx() / (dataPoints.size + 1) + } + val xOffset = remember(density) { // center points in the graph + xPositionRatio / dataPoints.size + } + // [END_EXCLUDE] + // Calculate baselines + val maxYBaseline = // [START_EXCLUDE] + remember(dataPoints) { + dataPoints.maxOrNull()?.let { + (maxValue - it) * yPositionRatio + } ?: 0f + } // [END_EXCLUDE] + val minYBaseline = // [START_EXCLUDE] + remember(dataPoints) { + dataPoints.minOrNull()?.let { + (maxValue - it) * yPositionRatio + } ?: 0f + } + // [END_EXCLUDE] + Layout( + content = {}, + modifier = Modifier.drawBehind { + // [START_EXCLUDE] + dataPoints.forEachIndexed { index, dataPoint -> + val rectSize = Size(60f, dataPoint * yPositionRatio) + val topLeftOffset = Offset( + x = xPositionRatio * (index + 1) - xOffset, + y = (maxValue - dataPoint) * yPositionRatio + ) + drawRect(Color(0xFF3DDC84), topLeftOffset, rectSize) + } + drawLine( + Color(0xFF073042), + start = Offset(0f, 0f), + end = Offset(0f, maxHeight.toPx()), + strokeWidth = 6f + ) + drawLine( + Color(0xFF073042), + start = Offset(0f, maxHeight.toPx()), + end = Offset(maxWidth.toPx(), maxHeight.toPx()), + strokeWidth = 6f + ) + // [END_EXCLUDE] + } + ) { _, constraints -> + with(constraints) { + layout( + width = if (hasBoundedWidth) maxWidth else minWidth, + height = if (hasBoundedHeight) maxHeight else minHeight, + // Custom AlignmentLines are set here. These are propagated + // to direct and indirect parent composables. + alignmentLines = mapOf( + MinChartValue to minYBaseline.roundToInt(), + MaxChartValue to maxYBaseline.roundToInt() + ) + ) {} + } + } + } + } +} +// [END android_compose_alignment_lines_example_bar_chart] + +// [START android_compose_alignment_lines_example_bar_chart_min_max] +@Composable +private fun BarChartMinMax( + dataPoints: List, + maxText: @Composable () -> Unit, + minText: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + Layout( + content = { + maxText() + minText() + // Set a fixed size to make the example easier to follow + BarChart(dataPoints, Modifier.size(200.dp)) + }, + modifier = modifier + ) { measurables, constraints -> + check(measurables.size == 3) + val placeables = measurables.map { + it.measure(constraints.copy(minWidth = 0, minHeight = 0)) + } + + val maxTextPlaceable = placeables[0] + val minTextPlaceable = placeables[1] + val barChartPlaceable = placeables[2] + + // Obtain the alignment lines from BarChart to position the Text + val minValueBaseline = barChartPlaceable[MinChartValue] + val maxValueBaseline = barChartPlaceable[MaxChartValue] + layout(constraints.maxWidth, constraints.maxHeight) { + maxTextPlaceable.placeRelative( + x = 0, + y = maxValueBaseline - (maxTextPlaceable.height / 2) + ) + minTextPlaceable.placeRelative( + x = 0, + y = minValueBaseline - (minTextPlaceable.height / 2) + ) + barChartPlaceable.placeRelative( + x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20, + y = 0 + ) + } + } +} +@Preview +@Composable +private fun ChartDataPreview() { + MaterialTheme { + BarChartMinMax( + dataPoints = listOf(4, 24, 15), + maxText = { Text("Max") }, + minText = { Text("Min") }, + modifier = Modifier.padding(24.dp) + ) + } +} +// [END android_compose_alignment_lines_example_bar_chart_min_max] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CommonLayoutExamples.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CommonLayoutExamples.kt new file mode 100644 index 000000000..98f5b03af --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CommonLayoutExamples.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.layouts + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.util.MaterialColors +import kotlin.random.Random + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +@Composable +@Preview +fun Layout_Graph_Vertical() { + val paddingModifier = Modifier.padding(4.dp) + // [START android_compose_layout_vertical_graph] + Column( + modifier = paddingModifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + val itemModifier = Modifier + .padding(4.dp) + .height(48.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialColors.Blue200) + repeat(7) { + val randomPercentage = Random.nextFloat() + Spacer( + modifier = itemModifier + .fillMaxWidth(randomPercentage) + .align(Alignment.End) + ) + } + } + // [END android_compose_layout_vertical_graph] +} + +@Composable +@Preview +fun Layout_Graph_Horizontal() { + val paddingModifier = Modifier.padding(4.dp) + // [START android_compose_layout_horizontal_graph] + Row( + modifier = paddingModifier.height(200.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + val itemModifier = Modifier + .padding(4.dp) + .weight(1f) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialColors.Blue200) + repeat(7) { index -> + val randomPercentage = Random.nextFloat() + Spacer( + modifier = itemModifier + .align(Alignment.Bottom) + .fillMaxHeight(randomPercentage) + ) + } + } + // [END android_compose_layout_horizontal_graph] +} + +@Composable +@Preview +fun Layout_StretchAll() { + // [START android_compose_layout_stretch_all] + Row( + modifier = Modifier.padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + val itemModifier = Modifier + .aspectRatio(1f) + .weight(1f) + .clip(RoundedCornerShape(8.dp)) + + Spacer(modifier = itemModifier.background(MaterialColors.Green200)) + Spacer(modifier = itemModifier.background(MaterialColors.Blue200)) + Spacer(modifier = itemModifier.background(MaterialColors.Pink200)) + Spacer(modifier = itemModifier.background(MaterialColors.Purple200)) + } + // [END android_compose_layout_stretch_all] +} + +@Composable +@Preview +fun Layout_StretchMiddleItem() { + // [START android_compose_layout_stretch_middle] + Row( + modifier = Modifier + .padding(4.dp) + .height(200.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + val itemModifier = Modifier + .fillMaxHeight() + .width(48.dp) + .clip(RoundedCornerShape(8.dp)) + + Spacer(modifier = itemModifier.background(MaterialColors.Green200)) + val middleStretchModifier = Modifier + .fillMaxHeight() + .weight(1f) + .clip(RoundedCornerShape(8.dp)) + Spacer(modifier = middleStretchModifier.background(MaterialColors.Blue200)) + Spacer(modifier = itemModifier.background(MaterialColors.Pink200)) + } + // [END android_compose_layout_stretch_middle] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintLayoutSnippets.kt new file mode 100644 index 000000000..d9763de19 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintLayoutSnippets.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("UNUSED_VARIABLE") + +package com.example.compose.snippets.layouts + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ChainStyle +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintSet +import androidx.constraintlayout.compose.layoutId + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +// [START android_compose_constraintlayout_basics] +@Composable +fun ConstraintLayoutContent() { + ConstraintLayout { + // Create references for the composables to constrain + val (button, text) = createRefs() + + Button( + onClick = { /* Do something */ }, + // Assign reference "button" to the Button composable + // and constrain it to the top of the ConstraintLayout + modifier = Modifier.constrainAs(button) { + top.linkTo(parent.top, margin = 16.dp) + } + ) { + Text("Button") + } + + // Assign reference "text" to the Text composable + // and constrain it to the bottom of the Button composable + Text( + "Text", + Modifier.constrainAs(text) { + top.linkTo(button.bottom, margin = 16.dp) + } + ) + } +} +// [END android_compose_constraintlayout_basics] + +// [START android_compose_constraintlayout_decoupled] +@Composable +fun DecoupledConstraintLayout() { + BoxWithConstraints { + val constraints = if (minWidth < 600.dp) { + decoupledConstraints(margin = 16.dp) // Portrait constraints + } else { + decoupledConstraints(margin = 32.dp) // Landscape constraints + } + + ConstraintLayout(constraints) { + Button( + onClick = { /* Do something */ }, + modifier = Modifier.layoutId("button") + ) { + Text("Button") + } + + Text("Text", Modifier.layoutId("text")) + } + } +} + +private fun decoupledConstraints(margin: Dp): ConstraintSet { + return ConstraintSet { + val button = createRefFor("button") + val text = createRefFor("text") + + constrain(button) { + top.linkTo(parent.top, margin = margin) + } + constrain(text) { + top.linkTo(button.bottom, margin) + } + } +} +// [END android_compose_constraintlayout_decoupled] + +@Composable +private fun ConstraintLayoutGuidelines() { + // [START android_compose_constraintlayout_guidelines] + ConstraintLayout { + // Create guideline from the start of the parent at 10% the width of the Composable + val startGuideline = createGuidelineFromStart(0.1f) + // Create guideline from the end of the parent at 10% the width of the Composable + val endGuideline = createGuidelineFromEnd(0.1f) + // Create guideline from 16 dp from the top of the parent + val topGuideline = createGuidelineFromTop(16.dp) + // Create guideline from 16 dp from the bottom of the parent + val bottomGuideline = createGuidelineFromBottom(16.dp) + } + // [END android_compose_constraintlayout_guidelines] +} + +@Composable +private fun ConstraintLayoutBarrier() { + // [START android_compose_constraintlayout_barrier] + ConstraintLayout { + val constraintSet = ConstraintSet { + val button = createRefFor("button") + val text = createRefFor("text") + + val topBarrier = createTopBarrier(button, text) + } + } + // [END android_compose_constraintlayout_barrier] +} + +@Composable +private fun ConstraintLayoutChain() { + // [START android_compose_constraintlayout_chain] + ConstraintLayout { + val constraintSet = ConstraintSet { + val button = createRefFor("button") + val text = createRefFor("text") + + val verticalChain = createVerticalChain(button, text, chainStyle = ChainStyle.Spread) + val horizontalChain = createHorizontalChain(button, text) + } + } + // [END android_compose_constraintlayout_chain] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintsModifiersSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintsModifiersSnippets.kt new file mode 100644 index 000000000..ad7275c1d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/ConstraintsModifiersSnippets.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.layouts + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +val containerModifier = Modifier.requiredSize(300.dp, 200.dp).background(Color(0xFFD1F1FE)) + +@Preview +@Composable +private fun Demo1() { + Box(containerModifier, contentAlignment = Alignment.Center) { + // [START android_compose_layout_constraints_modifiers_1] + Image( + painterResource(R.drawable.hero), + contentDescription = null, + Modifier + .fillMaxSize() + .size(50.dp) + ) + // [END android_compose_layout_constraints_modifiers_1] + } +} + +@Preview +@Composable +private fun Demo2() { + Box(containerModifier, contentAlignment = Alignment.Center) { + // [START android_compose_layout_constraints_modifiers_2] + Image( + painterResource(R.drawable.hero), + contentDescription = null, + Modifier + .fillMaxSize() + .wrapContentSize() + .size(50.dp) + ) + // [END android_compose_layout_constraints_modifiers_2] + } +} + +@Preview +@Composable +private fun Demo3() { + Box(containerModifier, contentAlignment = Alignment.Center) { + // [START android_compose_layout_constraints_modifiers_3] + Image( + painterResource(R.drawable.hero), + contentDescription = null, + Modifier + .clip(CircleShape) + .padding(10.dp) + .size(100.dp) + ) + // [END android_compose_layout_constraints_modifiers_3] + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CustomLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CustomLayoutSnippets.kt new file mode 100644 index 000000000..a92c0c2d8 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/CustomLayoutSnippets.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.layouts + +/* +* Copyright 2023 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.AlignmentLine +import androidx.compose.ui.layout.FirstBaseline +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.layout +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.layouts.CustomLayoutsSnippet2.firstBaselineToTop + +private object CustomLayoutsSnippet1 { + /* Can't be compiled without returning layout() from Modifier.layout. See next snippet for + possible changes.*/ + + // [START android_compose_layouts_modifier_basic] + fun Modifier.customLayoutModifier() = + layout { measurable, constraints -> + // [START_EXCLUDE] + val placeable = measurable.measure(constraints) + + layout(placeable.width, placeable.width) { + placeable.placeRelative(0, 0) + } + // [END_EXCLUDE] + } + // [END android_compose_layouts_modifier_basic] +} + +private object CustomLayoutsSnippet2 { + // [START android_compose_layouts_first_baseline] + fun Modifier.firstBaselineToTop( + firstBaselineToTop: Dp + ) = layout { measurable, constraints -> + // Measure the composable + val placeable = measurable.measure(constraints) + + // Check the composable has a first baseline + check(placeable[FirstBaseline] != AlignmentLine.Unspecified) + val firstBaseline = placeable[FirstBaseline] + + // Height of the composable with padding - first baseline + val placeableY = firstBaselineToTop.roundToPx() - firstBaseline + val height = placeable.height + placeableY + layout(placeable.width, height) { + // Where the composable gets placed + placeable.placeRelative(0, placeableY) + } + } + // [END android_compose_layouts_first_baseline] +} + +// [START android_compose_layouts_first_baseline_usage] +@Preview +@Composable +fun TextWithPaddingToBaselinePreview() { + MyApplicationTheme { + Text("Hi there!", Modifier.firstBaselineToTop(32.dp)) + } +} + +@Preview +@Composable +fun TextWithNormalPaddingPreview() { + MyApplicationTheme { + Text("Hi there!", Modifier.padding(top = 32.dp)) + } +} +// [END android_compose_layouts_first_baseline_usage] + +private object CustomLayoutsSnippet4 { + // [START android_compose_layouts_custom_layout_basic] + @Composable + fun MyBasicColumn( + modifier: Modifier = Modifier, + content: @Composable () -> Unit + ) { + Layout( + modifier = modifier, + content = content + ) { measurables, constraints -> + // measure and position children given constraints logic here + // [START_EXCLUDE] + val placeables = measurables.map { measurable -> + // Measure each children + measurable.measure(constraints) + } + + // Set the size of the layout as big as it can + layout(constraints.maxWidth, constraints.maxHeight) { + // Track the y co-ord we have placed children up to + var yPosition = 0 + + // Place children in the parent layout + placeables.forEach { placeable -> + // Position item on the screen + placeable.placeRelative(x = 0, y = yPosition) + + // Record the y co-ord placed up to + yPosition += placeable.height + } + } + // [END_EXCLUDE] + } + } + // [END android_compose_layouts_custom_layout_basic] +} + +private object CustomLayoutsSnippet5and6 { + // [START android_compose_layouts_custom_layout_more_details] + @Composable + fun MyBasicColumn( + modifier: Modifier = Modifier, + content: @Composable () -> Unit + ) { + Layout( + modifier = modifier, + content = content + ) { measurables, constraints -> + // Don't constrain child views further, measure them with given constraints + // List of measured children + val placeables = measurables.map { measurable -> + // Measure each children + measurable.measure(constraints) + } + + // Set the size of the layout as big as it can + layout(constraints.maxWidth, constraints.maxHeight) { + // Track the y co-ord we have placed children up to + var yPosition = 0 + + // Place children in the parent layout + placeables.forEach { placeable -> + // Position item on the screen + placeable.placeRelative(x = 0, y = yPosition) + + // Record the y co-ord placed up to + yPosition += placeable.height + } + } + } + } + // [END android_compose_layouts_custom_layout_more_details] + + // Snippet 6 + // [START android_compose_layouts_custom_layout_usage] + @Composable + fun CallingComposable(modifier: Modifier = Modifier) { + MyBasicColumn(modifier.padding(8.dp)) { + Text("MyBasicColumn") + Text("places items") + Text("vertically.") + Text("We've done it by hand!") + } + } + // [END android_compose_layouts_custom_layout_usage] +} + +/* +Fakes needed for snippets to build: + */ + +@Composable +private fun MyApplicationTheme(content: @Composable () -> Unit) { +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt new file mode 100644 index 000000000..8c682a601 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/FlowLayoutSnippets.kt @@ -0,0 +1,503 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.layouts + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowColumn +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.util.MaterialColors + +@Preview +@OptIn(ExperimentalLayoutApi::class) +// [START android_compose_flow_row_simple] +@Composable +private fun FlowRowSimpleUsageExample() { + FlowRow(modifier = Modifier.padding(8.dp)) { + ChipItem("Price: High to Low") + ChipItem("Avg rating: 4+") + ChipItem("Free breakfast") + ChipItem("Free cancellation") + ChipItem("£50 pn") + } +} +// [END android_compose_flow_row_simple] + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MainAxis_Default_Arrangement() { + FlowRow( + modifier = Modifier + .padding(8.dp) + .fillMaxSize(), + horizontalArrangement = Arrangement.Start + ) { + FlowItems() + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MainAxis_Space_BetweenArrangement() { + FlowRow( + modifier = Modifier + .padding(8.dp) + .fillMaxSize(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + FlowItems() + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MainAxis_Center_Arrangement() { + FlowRow( + modifier = Modifier + .padding(8.dp) + .fillMaxSize(), + horizontalArrangement = Arrangement.Center + ) { + FlowItems() + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MainAxis_End_Arrangement() { + FlowRow( + modifier = Modifier + .padding(8.dp) + .fillMaxSize(), + horizontalArrangement = Arrangement.End + ) { + FlowItems() + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MainAxis_SpaceAround_Arrangement() { + FlowRow( + modifier = Modifier + .padding(8.dp) + .fillMaxSize(), + horizontalArrangement = Arrangement.SpaceAround + ) { + FlowItems() + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MainAxis_Spacedby_Arrangement() { + FlowRow( + modifier = Modifier + .padding(8.dp) + .fillMaxSize(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FlowItems() + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MainAxis_VerticalArrangement() { + FlowRow( + modifier = Modifier + .padding(8.dp) + .size(400.dp) + .border(2.dp, Color.DarkGray) + .padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.Top + ) { + FlowItems() + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MainAxis_VerticalArrangement_Center() { + FlowRow( + modifier = Modifier + .padding(8.dp) + .size(400.dp) + .border(2.dp, Color.DarkGray) + .padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.Center + ) { + FlowItems() + } +} +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MainAxis_VerticalArrangement_Bottom() { + FlowRow( + modifier = Modifier + .padding(8.dp) + .size(400.dp) + .border(2.dp, Color.DarkGray) + .padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.Bottom + ) { + FlowItems() + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +private fun FlowRow_MaxItems() { + FlowRow( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + maxItemsInEachRow = 3 + ) { + FlowItems() + } +} + +// Demo items for Flow row / flow column +@Composable +private fun FlowItems() { + Item( + modifier = Modifier.width(50.dp), + color = MaterialColors.Amber300 + ) + Item( + modifier = Modifier.width(70.dp), + color = MaterialColors.Blue300 + ) + Item( + modifier = Modifier.width(96.dp), + color = MaterialColors.Cyan300 + ) + Item( + modifier = Modifier.width(40.dp), + color = MaterialColors.DeepPurple300 + ) + + Item( + modifier = Modifier.width(150.dp), + color = MaterialColors.Green300 + ) + Item( + modifier = Modifier.width(60.dp), + color = MaterialColors.Red300 + ) + Item( + modifier = Modifier.width(102.dp), + color = MaterialColors.Purple300 + ) + Item( + modifier = Modifier.width(42.dp), + color = MaterialColors.Teal300 + ) + + Item( + modifier = Modifier.width(50.dp), + color = MaterialColors.Pink300 + ) + Item( + modifier = Modifier.width(120.dp), + color = MaterialColors.Lime300 + ) + Item( + modifier = Modifier.width(110.dp), + color = MaterialColors.Yellow300 + ) + Item( + modifier = Modifier.width(90.dp), + color = MaterialColors.DeepPurple300 + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ChipItem(text: String, onClick: () -> Unit = {}) { + FilterChip( + modifier = Modifier.padding(end = 4.dp), + onClick = onClick, + leadingIcon = {}, + border = BorderStroke(1.dp, Color(0xFF3B3A3C)), + label = { + Text(text) + }, + selected = false + ) +} + +@Composable +fun Item(modifier: Modifier = Modifier, color: Color) { + Box( + modifier + .padding(4.dp) + .clip(RoundedCornerShape(16.dp)) + .background(color) + .height(48.dp) + ) +} + +@Composable +private fun FlowItemsDifferentHeights(modifier: Modifier = Modifier) { + Item( + modifier = modifier + .width(50.dp) + .height(48.dp), + color = MaterialColors.Amber300 + ) + Item( + modifier = modifier + .width(70.dp) + .height(100.dp), + color = MaterialColors.Blue300 + ) + Item( + modifier = modifier + .width(96.dp) + .height(120.dp), + color = MaterialColors.Cyan300 + ) + Item( + modifier = modifier + .width(40.dp) + .height(110.dp), + color = MaterialColors.DeepPurple300 + ) + + Item( + modifier = modifier + .width(150.dp) + .height(90.dp), + color = MaterialColors.Green300 + ) + Item( + modifier = modifier + .width(60.dp) + .height(70.dp), + color = MaterialColors.Red300 + ) + Item( + modifier = modifier + .width(102.dp) + .height(30.dp), + color = MaterialColors.Purple300 + ) + Item( + modifier = modifier + .width(42.dp) + .height(90.dp), + color = MaterialColors.Teal300 + ) + + Item( + modifier = modifier + .width(50.dp) + .height(40.dp), + color = MaterialColors.Pink300 + ) + Item( + modifier = modifier + .width(120.dp) + .height(30.dp), + color = MaterialColors.Lime300 + ) + Item( + modifier = modifier + .width(110.dp) + .height(50.dp), + color = MaterialColors.Yellow300 + ) + Item( + modifier = modifier + .width(90.dp) + .height(120.dp), + color = MaterialColors.DeepPurple300 + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +fun FlowLayout_Grid() { + // [START android_compose_flow_layout_grid] + val rows = 3 + val columns = 3 + FlowRow( + modifier = Modifier.padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + maxItemsInEachRow = rows + ) { + val itemModifier = Modifier + .padding(4.dp) + .height(80.dp) + .weight(1f) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialColors.Blue200) + repeat(rows * columns) { + Spacer(modifier = itemModifier) + } + } + // [END android_compose_flow_layout_grid] +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +@Preview +fun FlowLayout_AlternatingGrid() { + // [START android_compose_flow_layout_alternating_grid] + FlowRow( + modifier = Modifier.padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + maxItemsInEachRow = 2 + ) { + val itemModifier = Modifier + .padding(4.dp) + .height(80.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color.Blue) + repeat(6) { item -> + // if the item is the third item, don't use weight modifier, but rather fillMaxWidth + if ((item + 1) % 3 == 0) { + Spacer(modifier = itemModifier.fillMaxWidth()) + } else { + Spacer(modifier = itemModifier.weight(0.5f)) + } + } + } + // [END android_compose_flow_layout_alternating_grid] +} + +@OptIn(ExperimentalLayoutApi::class) +@Preview +@Composable +fun FlowLayout_FractionalSizing() { + // [START android_compose_flow_layout_fractional_sizing] + FlowRow( + modifier = Modifier.padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + maxItemsInEachRow = 3 + ) { + val itemModifier = Modifier + .clip(RoundedCornerShape(8.dp)) + Box( + modifier = itemModifier + .height(200.dp) + .width(60.dp) + .background(Color.Red) + ) + Box( + modifier = itemModifier + .height(200.dp) + .fillMaxWidth(0.7f) + .background(Color.Blue) + ) + Box( + modifier = itemModifier + .height(200.dp) + .weight(1f) + .background(Color.Magenta) + ) + } + // [END android_compose_flow_layout_fractional_sizing] +} + +@OptIn(ExperimentalLayoutApi::class) +@Preview +@Composable +fun FillMaxColumnWidth() { + // [START android_compose_flow_layouts_fill_max_column_width] + FlowColumn( + Modifier + .padding(20.dp) + .fillMaxHeight() + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + maxItemsInEachColumn = 5, + ) { + repeat(listDesserts.size) { + Box( + Modifier + .fillMaxColumnWidth() + .border(1.dp, Color.DarkGray, RoundedCornerShape(8.dp)) + .padding(8.dp) + ) { + + Text( + text = listDesserts[it], + fontSize = 18.sp, + modifier = Modifier.padding(3.dp) + ) + } + } + } + // [END android_compose_flow_layouts_fill_max_column_width] +} +private val listDesserts = listOf( + "Apple", + "Banana", + "Cupcake", + "Donut", + "Eclair", + "Froyo", + "Gingerbread", + "Honeycomb", + "Ice Cream Sandwich", + "Jellybean", + "KitKat", + "Lollipop", + "Marshmallow", + "Nougat", +) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt new file mode 100644 index 000000000..e893e8099 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt @@ -0,0 +1,265 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.layouts + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fitInside +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.BottomCenter +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.RectRulers +import androidx.compose.ui.layout.WindowInsetsRulers +import androidx.compose.ui.layout.innermostOf +import androidx.compose.ui.layout.layout +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +class InsetSnippetActivity : ComponentActivity() { + + // [START android_compose_insets_app_wide_safe_drawing] + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + enableEdgeToEdge() + + setContent { + Box(Modifier.safeDrawingPadding()) { + // the rest of the app + } + } + } + // [END android_compose_insets_app_wide_safe_drawing] +} + +@Preview +@Composable +fun SpacerHeightSnippet() { + // [START android_compose_insets_spacer_height] + LazyColumn( + Modifier.imePadding() + ) { + // Other content + item { + Spacer( + Modifier.windowInsetsBottomHeight( + WindowInsets.systemBars + ) + ) + } + } + // [END android_compose_insets_spacer_height] +} + +@Preview +@Composable +fun ConsumedFromSiblingsSnippet() { + // [START android_compose_insets_consumed_from_siblings] + Column(Modifier.verticalScroll(rememberScrollState())) { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars)) + + Column( + Modifier.consumeWindowInsets( + WindowInsets.systemBars.only(WindowInsetsSides.Vertical) + ) + ) { + // content + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + // [END android_compose_insets_consumed_from_siblings] +} + +@Preview +@Composable +fun ConsumedFromPaddingSnippet() { + // [START android_compose_insets_consumed_from_padding] + Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) { + // content + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + // [END android_compose_insets_consumed_from_padding] +} + +@Preview +@Composable +fun M3SupportScaffoldSnippet() { + // [START android_compose_insets_m3_scaffold] + Scaffold { innerPadding -> + // innerPadding contains inset information for you to use and apply + LazyColumn( + // consume insets as scaffold doesn't do it by default + modifier = Modifier.consumeWindowInsets(innerPadding), + contentPadding = innerPadding + ) { + // .. + } + } + // [END android_compose_insets_m3_scaffold] +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +fun OverrideDefaultInsetsSnippet() { + // [START android_compose_insets_override_defaults] + LargeTopAppBar( + windowInsets = WindowInsets(0, 0, 0, 0), + title = { + Text("Hi") + } + ) + // [END android_compose_insets_override_defaults] +} + +// [START android_compose_insets_rulers] +@Composable +fun WindowInsetsRulersDemo(modifier: Modifier) { + Box( + contentAlignment = BottomCenter, + modifier = modifier + .fillMaxSize() + // The mistake that causes issues downstream, as .padding doesn't consume insets. + // While it's correct to instead use .windowInsetsPadding(WindowInsets.navigationBars), + // assume it's difficult to identify this issue to see how WindowInsetsRulers can help. + .padding(WindowInsets.navigationBars.asPaddingValues()) + ) { + TextField( + value = "Demo IME Insets", + onValueChange = {}, + modifier = modifier + // Use alignToSafeDrawing() instead of .imePadding() to precisely place this child + // Composable without having to fix the parent upstream. + .alignToSafeDrawing() + + // .imePadding() + // .fillMaxWidth() + ) + } +} + +fun Modifier.alignToSafeDrawing(): Modifier { + return layout { measurable, constraints -> + if (constraints.hasBoundedWidth && constraints.hasBoundedHeight) { + val placeable = measurable.measure(constraints) + val width = placeable.width + val height = placeable.height + layout(width, height) { + val bottom = WindowInsetsRulers.SafeDrawing.current.bottom + .current(0f).roundToInt() - height + val right = WindowInsetsRulers.SafeDrawing.current.right + .current(0f).roundToInt() + val left = WindowInsetsRulers.SafeDrawing.current.left + .current(0f).roundToInt() + measurable.measure(Constraints.fixed(right - left, height)) + .place(left, bottom) + } + } else { + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + } +} +// [END android_compose_insets_rulers] + +// [START android_compose_insets_fit_inside] +@Composable +fun FitInsideDemo(modifier: Modifier) { + Box( + modifier = modifier + .fillMaxSize() + // Or DisplayCutout, Ime, NavigationBars, StatusBar, etc... + .fitInside(WindowInsetsRulers.SafeDrawing.current) + ) +} +// [END android_compose_insets_fit_inside] + +// [START android_compose_insets_rulers_ime] +@Composable +fun FitInsideWithImeDemo(modifier: Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .fitInside( + RectRulers.innermostOf( + WindowInsetsRulers.NavigationBars.current, + WindowInsetsRulers.Ime.current + ) + ) + ) { + TextField( + value = "Demo IME Insets", + onValueChange = {}, + modifier = modifier.align(Alignment.BottomStart).fillMaxWidth() + ) + } +} +// [END android_compose_insets_rulers_ime] + +// [START android_compose_insets_rulers_status_caption_bars] +@Composable +fun FitInsideWithStatusAndCaptionBarDemo(modifier: Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .fitInside( + RectRulers.innermostOf( + WindowInsetsRulers.StatusBars.current, + WindowInsetsRulers.CaptionBar.current + ) + ) + ) +} +// [END android_compose_insets_rulers_status_caption_bars] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt new file mode 100644 index 000000000..edca298d7 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") +package com.example.compose.snippets.layouts + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.IntrinsicMeasurable +import androidx.compose.ui.layout.IntrinsicMeasureScope +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp + +private object IntrinsicsSnippet1 { + + // [START android_compose_intrinsics_twotexts] + @Composable + fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) { + Row(modifier = modifier) { + Text( + modifier = Modifier + .weight(1f) + .padding(start = 4.dp) + .wrapContentWidth(Alignment.Start), + text = text1 + ) + VerticalDivider( + color = Color.Black, + modifier = Modifier.fillMaxHeight().width(1.dp) + ) + Text( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp) + .wrapContentWidth(Alignment.End), + + text = text2 + ) + } + } + // [END android_compose_intrinsics_twotexts] + + // @Preview + @Composable + fun TwoTextsPreview() { + MaterialTheme { + Surface { + TwoTexts(text1 = "Hi", text2 = "there") + } + } + } +} + +private object IntrinsicsSnippet2 { + + // [START android_compose_intrinsics_twotexts2] + @Composable + fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) { + Row(modifier = modifier.height(IntrinsicSize.Min)) { + Text( + modifier = Modifier + .weight(1f) + .padding(start = 4.dp) + .wrapContentWidth(Alignment.Start), + text = text1 + ) + VerticalDivider( + color = Color.Black, + modifier = Modifier.fillMaxHeight().width(1.dp) + ) + Text( + modifier = Modifier + .weight(1f) + .padding(end = 4.dp) + .wrapContentWidth(Alignment.End), + + text = text2 + ) + } + } + + // @Preview + @Composable + fun TwoTextsPreview() { + MaterialTheme { + Surface { + TwoTexts(text1 = "Hi", text2 = "there") + } + } + } + // [END android_compose_intrinsics_twotexts2] +} + +private object IntrinsicsSnippet3 { + // [START android_compose_intrinsics_custom_layout] + @Composable + fun MyCustomComposable( + modifier: Modifier = Modifier, + content: @Composable () -> Unit + ) { + Layout( + content = content, + modifier = modifier, + measurePolicy = object : MeasurePolicy { + override fun MeasureScope.measure( + measurables: List, + constraints: Constraints + ): MeasureResult { + // Measure and layout here + // [START_EXCLUDE] + TODO() // NOTE: Omit in the code snippets + // [END_EXCLUDE] + } + + override fun IntrinsicMeasureScope.minIntrinsicWidth( + measurables: List, + height: Int + ): Int { + // Logic here + // [START_EXCLUDE] + TODO() // NOTE: Omit in the code snippets + // [END_EXCLUDE] + } + + // Other intrinsics related methods have a default value, + // you can override only the methods that you need. + } + ) + } + // [END android_compose_intrinsics_custom_layout] +} + +private object IntrinsicsSnippet4 { + // [START android_compose_intrinsics_custom_modifier] + fun Modifier.myCustomModifier(/* ... */) = this then object : LayoutModifier { + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + // Measure and layout here + // [START_EXCLUDE] + TODO() // NOTE: Omit in the code snippets + // [END_EXCLUDE] + } + + override fun IntrinsicMeasureScope.minIntrinsicWidth( + measurable: IntrinsicMeasurable, + height: Int + ): Int { + // Logic here + // [START_EXCLUDE] + TODO() // NOTE: Omit in the code snippets + // [END_EXCLUDE] + } + + // Other intrinsics related methods have a default value, + // you can override only the methods that you need. + } + // [END android_compose_intrinsics_custom_modifier] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/LayoutBasicsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/LayoutBasicsSnippets.kt new file mode 100644 index 000000000..f1131dc00 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/LayoutBasicsSnippets.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.layouts + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +// [START android_compose_layout_basics_1] +@Composable +fun ArtistCard() { + Text("Alfred Sisley") + Text("3 minutes ago") +} +// [END android_compose_layout_basics_1] + +// [START android_compose_layout_basics_2] +@Composable +fun ArtistCardColumn() { + Column { + Text("Alfred Sisley") + Text("3 minutes ago") + } +} +// [END android_compose_layout_basics_2] + +// [START android_compose_layout_basics_3] +@Composable +fun ArtistCardRow(artist: Artist) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image(bitmap = artist.image, contentDescription = "Artist image") + Column { + Text(artist.name) + Text(artist.lastSeenOnline) + } + } +} + +// [START_EXCLUDE silent] +class Artist { + val image: ImageBitmap = ImageBitmap(0, 0) + val name = "" + val lastSeenOnline = "" +} +// [END_EXCLUDE] +// [END android_compose_layout_basics_3] + +// [START android_compose_layout_basics_4] +@Composable +fun ArtistAvatar(artist: Artist) { + Box { + Image(bitmap = artist.image, contentDescription = "Artist image") + Icon(Icons.Filled.Check, contentDescription = "Check mark") + } +} +// [END android_compose_layout_basics_4] + +// [START android_compose_layout_basics_5] +@Composable +fun ArtistCardArrangement(artist: Artist) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Image(bitmap = artist.image, contentDescription = "Artist image") + Column { /*...*/ } + } +} +// [END android_compose_layout_basics_5] + +// [START android_compose_layout_basics_6] +@Composable +fun SearchResult() { + Row { + Image( + // [START_EXCLUDE] + painter = painterResource(id = R.drawable.dog), contentDescription = "" + // [END_EXCLUDE] + ) + Column { + Text( + // [START_EXCLUDE] + "Hello" + // [END_EXCLUDE] + ) + Text( + // [START_EXCLUDE] + "World" + // [END_EXCLUDE] + ) + } + } +} +// [END android_compose_layout_basics_6] + +// [START android_compose_layout_basics_7] +@Composable +fun ArtistCardModifiers( + artist: Artist, + onClick: () -> Unit +) { + val padding = 16.dp + Column( + Modifier + .clickable(onClick = onClick) + .padding(padding) + .fillMaxWidth() + ) { + Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ } + Spacer(Modifier.size(padding)) + Card( + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + ) { /*...*/ } + } +} +// [END android_compose_layout_basics_7] + +// [START android_compose_layout_basics_8] +@Composable +fun WithConstraintsComposable() { + BoxWithConstraints { + Text("My minHeight is $minHeight while my maxWidth is $maxWidth") + } +} +// [END android_compose_layout_basics_8] + +// [START android_compose_layout_basics_9] +@Composable +fun HomeScreen(/*...*/) { + ModalNavigationDrawer(drawerContent = { /* ... */ }) { + Scaffold( + topBar = { /*...*/ } + ) { contentPadding -> + // [START_EXCLUDE] + Box(Modifier.padding(contentPadding)) { } + // [END_EXCLUDE] + } + } +} +// [END android_compose_layout_basics_9] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/MaterialLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/MaterialLayoutSnippets.kt new file mode 100644 index 000000000..b027c6eae --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/MaterialLayoutSnippets.kt @@ -0,0 +1,410 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.layouts + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Image +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +// [START android_compose_layout_material_theme] +@Composable +fun MyApp() { + MaterialTheme { + // Material Components like Button, Card, Switch, etc. + } +} +// [END android_compose_layout_material_theme] + +@Composable +fun ButtonDemo() { + // [START android_compose_layout_material_button] + Button( + onClick = { /* ... */ }, + // Uses ButtonDefaults.ContentPadding by default + contentPadding = PaddingValues( + start = 20.dp, + top = 12.dp, + end = 20.dp, + bottom = 12.dp + ) + ) { + // Inner content including an icon and a text label + Icon( + Icons.Filled.Favorite, + contentDescription = "Favorite", + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text("Like") + } + // [END android_compose_layout_material_button] +} + +@Composable +fun ExtendedFabDemo() { + // [START android_compose_layout_material_fab] + ExtendedFloatingActionButton( + onClick = { /* ... */ }, + icon = { + Icon( + Icons.Filled.Favorite, + contentDescription = "Favorite" + ) + }, + text = { Text("Like") } + ) + // [END android_compose_layout_material_fab] +} + +@Composable +fun ScaffoldDemo() { + // [START android_compose_layout_material_scaffold] + Scaffold(/* ... */) { contentPadding -> + // Screen content + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + } + // [END android_compose_layout_material_scaffold] +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ScaffoldTopAppBarDemo() { + // [START android_compose_layout_material_appbar] + Scaffold( + topBar = { + TopAppBar(title = { + Text("My App") + }) + } + ) { contentPadding -> + // Screen content + // [START_EXCLUDE silent] + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + // [END_EXCLUDE] + } + // [END android_compose_layout_material_appbar] +} + +@Composable +fun ScaffoldBottomBarDemo() { + // [START android_compose_layout_material_bottombar] + Scaffold( + bottomBar = { + BottomAppBar { /* Bottom app bar content */ } + } + ) { contentPadding -> + // Screen content + // [START_EXCLUDE silent] + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + // [END_EXCLUDE] + } + // [END android_compose_layout_material_bottombar] +} + +@Composable +fun ScaffoldFabDemo() { + // [START android_compose_layout_material_scaffold_fab] + Scaffold( + floatingActionButton = { + FloatingActionButton(onClick = { /* ... */ }) { + /* FAB content */ + } + } + ) { contentPadding -> + // Screen content + // [START_EXCLUDE silent] + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + // [END_EXCLUDE] + } + // [END android_compose_layout_material_scaffold_fab] +} + +@Composable +fun ScaffoldFabPositionDemo() { + // [START android_compose_layout_material_scaffold_fab_position] + Scaffold( + floatingActionButton = { + FloatingActionButton(onClick = { /* ... */ }) { + /* FAB content */ + } + }, + floatingActionButtonPosition = FabPosition.Center + ) { contentPadding -> + // Screen content + // [START_EXCLUDE silent] + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + // [END_EXCLUDE] + } + // [END android_compose_layout_material_scaffold_fab_position] +} + +@Composable +fun ScaffoldFabAndBottomBarDemo() { + // [START android_compose_layout_material_scaffold_fab_docked] + Scaffold( + floatingActionButton = { + FloatingActionButton(onClick = { /* ... */ }) { + /* FAB content */ + } + }, + bottomBar = { + BottomAppBar { /* Bottom app bar content */ } + } + ) { contentPadding -> + // Screen content + // [START_EXCLUDE silent] + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + // [END_EXCLUDE] + } + // [END android_compose_layout_material_scaffold_fab_docked] +} + +@Composable +fun ScaffoldSnackbarDemo() { + // [START android_compose_layout_material_snackbar] + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Show snackbar") }, + icon = { Icon(Icons.Filled.Image, contentDescription = "") }, + onClick = { + scope.launch { + snackbarHostState.showSnackbar("Snackbar") + } + } + ) + } + ) { contentPadding -> + // Screen content + // [START_EXCLUDE silent] + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + // [END_EXCLUDE] + } + // [END android_compose_layout_material_snackbar] +} + +@Composable +fun ScaffoldSnackbarResultDemo() { + // [START android_compose_layout_material_snackbar_result] + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Show snackbar") }, + icon = { Icon(Icons.Filled.Image, contentDescription = "") }, + onClick = { + scope.launch { + val result = snackbarHostState + .showSnackbar( + message = "Snackbar", + actionLabel = "Action", + // Defaults to SnackbarDuration.Short + duration = SnackbarDuration.Indefinite + ) + when (result) { + SnackbarResult.ActionPerformed -> { + /* Handle snackbar action performed */ + } + SnackbarResult.Dismissed -> { + /* Handle snackbar dismissed */ + } + } + } + } + ) + } + ) { contentPadding -> + // Screen content + // [START_EXCLUDE silent] + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + // [END_EXCLUDE] + } + // [END android_compose_layout_material_snackbar_result] +} + +@Composable +fun DrawerDemo() { + // [START android_compose_layout_material_modal_drawer] + ModalNavigationDrawer( + drawerContent = { + ModalDrawerSheet { + Text("Drawer title", modifier = Modifier.padding(16.dp)) + HorizontalDivider() + NavigationDrawerItem( + label = { Text(text = "Drawer Item") }, + selected = false, + onClick = { /*TODO*/ } + ) + // ...other drawer items + } + } + ) { + // Screen content + } + // [END android_compose_layout_material_modal_drawer] +} + +@Composable +fun DrawerGesturesDemo() { + // [START android_compose_layout_material_modal_drawer_gestures] + ModalNavigationDrawer( + drawerContent = { + ModalDrawerSheet { + // Drawer contents + } + }, + gesturesEnabled = false + ) { + // Screen content + } + // [END android_compose_layout_material_modal_drawer_gestures] +} + +@Composable +fun DrawerStateDemo() { + // [START android_compose_layout_material_modal_drawer_programmatic] + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope = rememberCoroutineScope() + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet { /* Drawer content */ } + }, + ) { + Scaffold( + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Show drawer") }, + icon = { Icon(Icons.Filled.Add, contentDescription = "") }, + onClick = { + scope.launch { + drawerState.apply { + if (isClosed) open() else close() + } + } + } + ) + } + ) { contentPadding -> + // Screen content + // [START_EXCLUDE silent] + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + // [END_EXCLUDE] + } + } + // [END android_compose_layout_material_modal_drawer_programmatic] +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BottomSheetDemo() { + // [START android_compose_layout_material_bottom_sheet] + ModalBottomSheet(onDismissRequest = { /* Executed when the sheet is dismissed */ }) { + // Sheet content + } + // [END android_compose_layout_material_bottom_sheet] + + // [START android_compose_layout_material_bottom_sheet2] + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + var showBottomSheet by remember { mutableStateOf(false) } + Scaffold( + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Show bottom sheet") }, + icon = { Icon(Icons.Filled.Add, contentDescription = "") }, + onClick = { + showBottomSheet = true + } + ) + } + ) { contentPadding -> + // Screen content + // [START_EXCLUDE silent] + Box(modifier = Modifier.padding(contentPadding)) { /* ... */ } + // [END_EXCLUDE] + + if (showBottomSheet) { + ModalBottomSheet( + onDismissRequest = { + showBottomSheet = false + }, + sheetState = sheetState + ) { + // Sheet content + Button(onClick = { + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) { + showBottomSheet = false + } + } + }) { + Text("Hide bottom sheet") + } + } + } + } + // [END android_compose_layout_material_bottom_sheet2] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt new file mode 100644 index 000000000..e09fe456d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt @@ -0,0 +1,569 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.layouts + +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.PagerSnapDistance +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import coil.compose.rememberAsyncImagePainter +import com.example.compose.snippets.util.rememberRandomSampleImageUrl +import kotlin.math.absoluteValue +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/* +* Copyright 2023 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +@Composable +fun PagerExamples() { + AutoAdvancePager( + listOf( + Color.Red, + Color.Gray, + Color.Green, + Color.White + ) + ) +} + +@Preview +@Composable +fun HorizontalPagerSample() { + // [START android_compose_layouts_pager_horizontal_basic] + // Display 10 items + val pagerState = rememberPagerState(pageCount = { + 10 + }) + HorizontalPager(state = pagerState) { page -> + // Our page content + Text( + text = "Page: $page", + modifier = Modifier.fillMaxWidth() + ) + } + // [END android_compose_layouts_pager_horizontal_basic] +} + +@Preview +@Composable +fun VerticalPagerSample() { + // [START android_compose_layouts_pager_vertical_basic] + // Display 10 items + val pagerState = rememberPagerState(pageCount = { + 10 + }) + VerticalPager(state = pagerState) { page -> + // Our page content + Text( + text = "Page: $page", + modifier = Modifier.fillMaxWidth() + ) + } + // [END android_compose_layouts_pager_vertical_basic] +} + +@Preview +@Composable +fun PagerScrollToItem() { + Box { + // [START android_compose_layouts_pager_scroll] + val pagerState = rememberPagerState(pageCount = { + 10 + }) + HorizontalPager(state = pagerState) { page -> + // Our page content + Text( + text = "Page: $page", + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + ) + } + + // scroll to page + val coroutineScope = rememberCoroutineScope() + Button(onClick = { + coroutineScope.launch { + // Call scroll to on pagerState + pagerState.scrollToPage(5) + } + }, modifier = Modifier.align(Alignment.BottomCenter)) { + Text("Jump to Page 5") + } + // [END android_compose_layouts_pager_scroll] + } +} + +@Preview +@Composable +fun PagerAnimateToItem() { + Box { + // [START android_compose_layouts_pager_scroll_animate] + val pagerState = rememberPagerState(pageCount = { + 10 + }) + + HorizontalPager(state = pagerState) { page -> + // Our page content + Text( + text = "Page: $page", + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + ) + } + + // scroll to page + val coroutineScope = rememberCoroutineScope() + Button(onClick = { + coroutineScope.launch { + // Call scroll to on pagerState + pagerState.animateScrollToPage(5) + } + }, modifier = Modifier.align(Alignment.BottomCenter)) { + Text("Jump to Page 5") + } + // [END android_compose_layouts_pager_scroll_animate] + } +} + +@Preview +@Composable +fun PageChangesSample() { + // [START android_compose_layouts_pager_notify_page_changes] + val pagerState = rememberPagerState(pageCount = { + 10 + }) + + LaunchedEffect(pagerState) { + // Collect from the a snapshotFlow reading the currentPage + snapshotFlow { pagerState.currentPage }.collect { page -> + // Do something with each page change, for example: + // viewModel.sendPageSelectedEvent(page) + Log.d("Page change", "Page changed to $page") + } + } + + VerticalPager( + state = pagerState, + ) { page -> + Text(text = "Page: $page") + } + // [END android_compose_layouts_pager_notify_page_changes] +} + +@Composable +@Preview +fun PagerWithTabsExample() { + val pages = listOf("Movies", "Books", "Shows", "Fun") + // [START android_compose_layouts_pager_tabs] + val pagerState = rememberPagerState(pageCount = { + pages.size + }) + + PrimaryTabRow( + // Our selected tab is our current page + selectedTabIndex = pagerState.currentPage, + ) { + // Add tabs for all of our pages + pages.forEachIndexed { index, title -> + Tab( + text = { Text(title) }, + selected = pagerState.currentPage == index, + onClick = { }, + ) + } + } + + HorizontalPager( + state = pagerState, + ) { page -> + Text("Page: ${pages[page]}") + } + // [END android_compose_layouts_pager_tabs] +} + +@Preview +@Composable +fun PagerWithEffect() { + // [START android_compose_layouts_pager_transformation] + val pagerState = rememberPagerState(pageCount = { + 4 + }) + HorizontalPager(state = pagerState) { page -> + Card( + Modifier + .size(200.dp) + .graphicsLayer { + // Calculate the absolute offset for the current page from the + // scroll position. We use the absolute value which allows us to mirror + // any effects for both directions + val pageOffset = ( + (pagerState.currentPage - page) + pagerState + .currentPageOffsetFraction + ).absoluteValue + + // We animate the alpha, between 50% and 100% + alpha = lerp( + start = 0.5f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + } + ) { + // Card content + } + } + // [END android_compose_layouts_pager_transformation] +} + +@Composable +@Preview +fun PagerStartPadding() { + // [START android_compose_layouts_pager_padding_start] + val pagerState = rememberPagerState(pageCount = { + 4 + }) + HorizontalPager( + state = pagerState, + contentPadding = PaddingValues(start = 64.dp), + ) { page -> + // page content + } + // [END android_compose_layouts_pager_padding_start] +} + +@Preview +@Composable +fun PagerHorizontalPadding() { + // [START android_compose_layouts_pager_padding_horizontal] + val pagerState = rememberPagerState(pageCount = { + 4 + }) + HorizontalPager( + state = pagerState, + contentPadding = PaddingValues(horizontal = 32.dp), + ) { page -> + // page content + } + // [END android_compose_layouts_pager_padding_horizontal] +} + +@Preview +@Composable +fun PagerEndPadding() { + // [START android_compose_layouts_pager_padding_end] + val pagerState = rememberPagerState(pageCount = { + 4 + }) + HorizontalPager( + state = pagerState, + contentPadding = PaddingValues(end = 64.dp), + ) { page -> + // page content + } + // [END android_compose_layouts_pager_padding_end] +} + +@Preview +@Composable +fun PagerCustomSizes() { + // [START android_compose_layouts_pager_custom_size] + val pagerState = rememberPagerState(pageCount = { + 4 + }) + HorizontalPager( + state = pagerState, + pageSize = PageSize.Fixed(100.dp) + ) { page -> + // page content + } + // [END android_compose_layouts_pager_custom_size] +} + +@Preview +@Composable +fun PagerWithTabs() { + // [START android_compose_layouts_pager_with_tabs] + val pagerState = rememberPagerState(pageCount = { + 4 + }) + HorizontalPager( + state = pagerState, + ) { page -> + // page content + } + // [END android_compose_layouts_pager_with_tabs] +} +@Preview +@Composable +fun PagerIndicator() { + Box(modifier = Modifier.fillMaxSize()) { + // [START android_compose_pager_indicator] + val pagerState = rememberPagerState(pageCount = { + 4 + }) + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize() + ) { page -> + // Our page content + Text( + text = "Page: $page", + ) + } + Row( + Modifier + .wrapContentHeight() + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.Center + ) { + repeat(pagerState.pageCount) { iteration -> + val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray + Box( + modifier = Modifier + .padding(2.dp) + .clip(CircleShape) + .background(color) + .size(16.dp) + ) + } + } + // [END android_compose_pager_indicator] + } +} + +// [START android_compose_autoadvancepager] +@Composable +fun AutoAdvancePager(pageItems: List, modifier: Modifier = Modifier) { + Box(modifier = Modifier.fillMaxSize()) { + val pagerState = rememberPagerState(pageCount = { pageItems.size }) + val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState() + + val pageInteractionSource = remember { MutableInteractionSource() } + val pageIsPressed by pageInteractionSource.collectIsPressedAsState() + + // Stop auto-advancing when pager is dragged or one of the pages is pressed + val autoAdvance = !pagerIsDragged && !pageIsPressed + + if (autoAdvance) { + LaunchedEffect(pagerState, pageInteractionSource) { + while (true) { + delay(2000) + val nextPage = (pagerState.currentPage + 1) % pageItems.size + pagerState.animateScrollToPage(nextPage) + } + } + } + + HorizontalPager( + state = pagerState + ) { page -> + Text( + text = "Page: $page", + textAlign = TextAlign.Center, + modifier = modifier + .fillMaxSize() + .background(pageItems[page]) + .clickable( + interactionSource = pageInteractionSource, + indication = LocalIndication.current + ) { + // Handle page click + } + .wrapContentSize(align = Alignment.Center) + ) + } + + PagerIndicator(pageItems.size, pagerState.currentPage) + } +} +// [END android_compose_autoadvancepager] + +@Preview +@Composable +private fun AutoAdvancePagerPreview() { + val pageItems: List = listOf( + Color.Red, + Color.Gray, + Color.Green, + Color.White + ) + AutoAdvancePager(pageItems = pageItems) +} + +// [START android_compose_pagerindicator] +@Composable +fun PagerIndicator(pageCount: Int, currentPageIndex: Int, modifier: Modifier = Modifier) { + Box(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.Center + ) { + repeat(pageCount) { iteration -> + val color = if (currentPageIndex == iteration) Color.DarkGray else Color.LightGray + Box( + modifier = modifier + .padding(2.dp) + .clip(CircleShape) + .background(color) + .size(16.dp) + ) + } + } + } +} +// [END android_compose_pagerindicator] + +@Preview +@Composable +private fun PagerIndicatorPreview() { + PagerIndicator(pageCount = 4, currentPageIndex = 1) +} + +// [START android_compose_pager_custom_page_size] +private val threePagesPerViewport = object : PageSize { + override fun Density.calculateMainAxisPageSize( + availableSpace: Int, + pageSpacing: Int + ): Int { + return (availableSpace - 2 * pageSpacing) / 3 + } +} +// [END android_compose_pager_custom_page_size] + +@Preview +@Composable +private fun CustomSnapDistance() { + // [START android_compose_pager_custom_snap_distance] + val pagerState = rememberPagerState(pageCount = { 10 }) + + val fling = PagerDefaults.flingBehavior( + state = pagerState, + pagerSnapDistance = PagerSnapDistance.atMost(10) + ) + + Column(modifier = Modifier.fillMaxSize()) { + HorizontalPager( + state = pagerState, + pageSize = PageSize.Fixed(200.dp), + beyondViewportPageCount = 10, + flingBehavior = fling + ) { + PagerSampleItem(page = it) + } + } + // [END android_compose_pager_custom_snap_distance] +} + +@Composable +internal fun PagerSampleItem( + page: Int, + modifier: Modifier = Modifier, +) { + Box(modifier.fillMaxSize()) { + // Our page content, displaying a random image + Image( + painter = rememberAsyncImagePainter(model = rememberRandomSampleImageUrl(width = 600)), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier.matchParentSize() + ) + + // Displays the page index + Text( + text = page.toString(), + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + .background(MaterialTheme.colorScheme.surface, RoundedCornerShape(4.dp)) + .sizeIn(minWidth = 40.dp, minHeight = 40.dp) + .padding(8.dp) + .wrapContentSize(Alignment.Center) + ) + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/SystemBarProtectionSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/SystemBarProtectionSnippets.kt new file mode 100644 index 000000000..429e7ffd1 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/SystemBarProtectionSnippets.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.layouts + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.designsystems.MyTheme + +// [START android_compose_system_bar_protection] +class SystemBarProtectionSnippets : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // enableEdgeToEdge sets window.isNavigationBarContrastEnforced = true + // which is used to add a translucent scrim to three-button navigation + enableEdgeToEdge() + + setContent { + MyTheme { + // Main content + MyContent() + + // After drawing main content, draw status bar protection + StatusBarProtection() + } + } + } +} + +@Composable +private fun StatusBarProtection( + color: Color = MaterialTheme.colorScheme.surfaceContainer, + heightProvider: () -> Float = calculateGradientHeight(), +) { + + Canvas(Modifier.fillMaxSize()) { + val calculatedHeight = heightProvider() + val gradient = Brush.verticalGradient( + colors = listOf( + color.copy(alpha = 1f), + color.copy(alpha = .8f), + Color.Transparent + ), + startY = 0f, + endY = calculatedHeight + ) + drawRect( + brush = gradient, + size = Size(size.width, calculatedHeight), + ) + } +} + +@Composable +fun calculateGradientHeight(): () -> Float { + val statusBars = WindowInsets.statusBars + val density = LocalDensity.current + return { statusBars.getTop(density).times(1.2f) } +} +// [END android_compose_system_bar_protection] + +@Composable +fun MyContent() { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + val colorScheme = MaterialTheme.colorScheme + val loremIpsum = LoremIpsum() + + LazyColumn( + contentPadding = innerPadding + ) { + items(13) { index -> + Card( + modifier = Modifier.fillMaxWidth().padding(8.dp), + colors = CardDefaults.cardColors( + containerColor = colorScheme.surfaceVariant + ) + ) { + Column(modifier = Modifier.padding(8.dp)) { + Text( + text = loremIpsum.titles[index], + style = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + color = colorScheme.onSurfaceVariant + ) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = loremIpsum.descriptions[index], + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + color = colorScheme.onSurfaceVariant + ) + ) + } + } + } + } + } +} + +class LoremIpsum { + private val lorem = "First item of the list." + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + + "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo." + + "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt." + + "Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem." + + "Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?" + + "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga." + + "Et harum quidem rerum facilis est et expedita distinctio." + + "Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus." + + "Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae." + + "Last item of the list." + val descriptions = lorem.split(".").map { description -> + description.trim() + } + val titles = descriptions.map { sentence -> + sentence.trim().split(" ").take(2).joinToString(" ") + } +} + +@Preview +@Composable +fun MyContentPreview() { + MyTheme { + MyContent() + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/lifecycle/LifecycleSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/lifecycle/LifecycleSnippets.kt new file mode 100644 index 000000000..c35558976 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/lifecycle/LifecycleSnippets.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.lifecycle + +import android.media.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.key +import com.example.compose.snippets.sideeffects.Movie +import com.example.compose.snippets.sideeffects.Result +import com.example.compose.snippets.sideeffects.loadNetworkImage + +// [START android_compose_lifecycle_1] +@Composable +fun MyComposable() { + Column { + Text("Hello") + Text("World") + } +} +// [END android_compose_lifecycle_1] + +// [START android_compose_lifecycle_2] +@Composable +fun LoginScreen(showError: Boolean) { + if (showError) { + LoginError() + } + LoginInput() // This call site affects where LoginInput is placed in Composition +} + +@Composable +fun LoginInput() { /* ... */ } + +@Composable +fun LoginError() { /* ... */ } +// [END android_compose_lifecycle_2] + +// [START android_compose_lifecycle_3] +@Composable +fun MoviesScreen(movies: List) { + Column { + for (movie in movies) { + // MovieOverview composables are placed in Composition given its + // index position in the for loop + MovieOverview(movie) + } + } +} +// [END android_compose_lifecycle_3] + +// [START android_compose_lifecycle_4] +@Composable +fun MovieOverview(movie: Movie) { + Column { + // Side effect explained later in the docs. If MovieOverview + // recomposes, while fetching the image is in progress, + // it is cancelled and restarted. + val image = loadNetworkImage(movie.url) + MovieHeader(image) + + /* ... */ + } +} + +// [START_EXCLUDE silent] +@Composable +fun MovieHeader(image: State>) { } +// [END_EXCLUDE] +// [END android_compose_lifecycle_4] + +// [START android_compose_lifecycle_5] +@Composable +fun MoviesScreenWithKey(movies: List) { + Column { + for (movie in movies) { + key(movie.id) { // Unique ID for this movie + MovieOverview(movie) + } + } + } +} +// [END android_compose_lifecycle_5] + +// [START android_compose_lifecycle_6] +@Composable +fun MoviesScreenLazy(movies: List) { + LazyColumn { + items(movies, key = { movie -> movie.id }) { movie -> + MovieOverview(movie) + } + } +} +// [END android_compose_lifecycle_6] + +// [START android_compose_lifecycle_7] +// Marking the type as stable to favor skipping and smart recompositions. +@Stable +interface UiState> { + val value: T? + val exception: Throwable? + + val hasError: Boolean + get() = exception != null +} +// [END android_compose_lifecycle_7] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/lists/AnimatedOrderedList.kt b/compose/snippets/src/main/java/com/example/compose/snippets/lists/AnimatedOrderedList.kt new file mode 100644 index 000000000..4d60e66b2 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/lists/AnimatedOrderedList.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.lists + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class AnimatedOrderedListViewModel : ViewModel() { + private val _data = listOf("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten") + private val _displayedItems: MutableStateFlow> = MutableStateFlow(_data) + val displayedItems: StateFlow> = _displayedItems + + fun resetOrder() { + _displayedItems.value = _data.filter { it in _displayedItems.value } + } + + fun sortAlphabetically() { + _displayedItems.value = _displayedItems.value.sortedBy { it } + } + + fun sortByLength() { + _displayedItems.value = _displayedItems.value.sortedBy { it.length } + } + + fun addItem() { + // Avoid duplicate items + val remainingItems = _data.filter { it !in _displayedItems.value } + if (remainingItems.isNotEmpty()) _displayedItems.value += remainingItems.first() + } + + fun removeItem() { + _displayedItems.value = _displayedItems.value.dropLast(1) + } +} + +@Composable +fun AnimatedOrderedListScreen( + viewModel: AnimatedOrderedListViewModel, + modifier: Modifier = Modifier, +) { + val displayedItems by viewModel.displayedItems.collectAsStateWithLifecycle() + + ListAnimatedItemsExample( + displayedItems, + onAddItem = viewModel::addItem, + onRemoveItem = viewModel::removeItem, + resetOrder = viewModel::resetOrder, + onSortAlphabetically = viewModel::sortAlphabetically, + onSortByLength = viewModel::sortByLength, + modifier = modifier + ) +} + +// [START android_compose_layouts_list_listanimateditems] +@Composable +fun ListAnimatedItems( + items: List, + modifier: Modifier = Modifier +) { + LazyColumn(modifier) { + // Use a unique key per item, so that animations work as expected. + items(items, key = { it }) { + ListItem( + headlineContent = { Text(it) }, + modifier = Modifier + .animateItem( + // Optionally add custom animation specs + ) + .fillParentMaxWidth() + .padding(horizontal = 8.dp, vertical = 0.dp), + ) + } + } +} +// [END android_compose_layouts_list_listanimateditems] + +// [START android_compose_layouts_list_listanimateditemsexample] +@Composable +private fun ListAnimatedItemsExample( + data: List, + modifier: Modifier = Modifier, + onAddItem: () -> Unit = {}, + onRemoveItem: () -> Unit = {}, + resetOrder: () -> Unit = {}, + onSortAlphabetically: () -> Unit = {}, + onSortByLength: () -> Unit = {}, +) { + val canAddItem = data.size < 10 + val canRemoveItem = data.isNotEmpty() + + Scaffold(modifier) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + // Buttons that change the value of displayedItems. + AddRemoveButtons(canAddItem, canRemoveItem, onAddItem, onRemoveItem) + OrderButtons(resetOrder, onSortAlphabetically, onSortByLength) + + // List that displays the values of displayedItems. + ListAnimatedItems(data) + } + } +} +// [END android_compose_layouts_list_listanimateditemsexample] + +// [START android_compose_layouts_list_addremovebuttons] +@Composable +private fun AddRemoveButtons( + canAddItem: Boolean, + canRemoveItem: Boolean, + onAddItem: () -> Unit, + onRemoveItem: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Button(enabled = canAddItem, onClick = onAddItem) { + Text("Add Item") + } + Spacer(modifier = Modifier.padding(25.dp)) + Button(enabled = canRemoveItem, onClick = onRemoveItem) { + Text("Delete Item") + } + } +} +// [END android_compose_layouts_list_addremovebuttons] + +// [START android_compose_layouts_list_orderbuttons] +@Composable +private fun OrderButtons( + resetOrder: () -> Unit, + orderAlphabetically: () -> Unit, + orderByLength: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + var selectedIndex by remember { mutableIntStateOf(0) } + val options = listOf("Reset", "Alphabetical", "Length") + + SingleChoiceSegmentedButtonRow { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = options.size + ), + onClick = { + Log.d("AnimatedOrderedList", "selectedIndex: $selectedIndex") + selectedIndex = index + when (options[selectedIndex]) { + "Reset" -> resetOrder() + "Alphabetical" -> orderAlphabetically() + "Length" -> orderByLength() + } + }, + selected = index == selectedIndex + ) { + Text(label) + } + } + } + } +} +// [END android_compose_layouts_list_orderbuttons] + +@Preview +@Composable +fun AnimatedOrderedListScreenPreview() { + val viewModel = remember { AnimatedOrderedListViewModel() } + AnimatedOrderedListScreen(viewModel) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/lists/LazyListSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/lists/LazyListSnippets.kt new file mode 100644 index 000000000..b41ca5942 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/lists/LazyListSnippets.kt @@ -0,0 +1,858 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "UNUSED_VARIABLE") + +package com.example.compose.snippets.lists + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.border +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import androidx.paging.Pager +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import coil.compose.AsyncImage +import coil.compose.rememberAsyncImagePainter +import com.example.compose.snippets.util.randomSampleImageUrl +import kotlin.random.Random +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import org.w3c.dom.Text + +private object ListsSnippetsColumn { + // [START android_compose_layouts_list_column] + @Composable + fun MessageList(messages: List) { + Column { + messages.forEach { message -> + MessageRow(message) + } + } + } + // [END android_compose_layouts_list_column] +} + +@Composable +private fun ListsSnippetsLazyListScope1() { + // [START android_compose_layouts_lazy_column_basic] + LazyColumn { + // Add a single item + item { + Text(text = "First item") + } + + // Add 5 items + items(5) { index -> + Text(text = "Item: $index") + } + + // Add another single item + item { + Text(text = "Last item") + } + } + // [END android_compose_layouts_lazy_column_basic] +} + +private object ListsSnippetsLazyListScope2 { + @Composable + fun MessageList(messages: List) { + // [START android_compose_layouts_lazy_column_basic_extension] + /** + * import androidx.compose.foundation.lazy.items + */ + LazyColumn { + items(messages) { message -> + MessageRow(message) + } + } + // [END android_compose_layouts_lazy_column_basic_extension] + } +} +@Composable +private fun GridItemSpanExample() { + // [START android_compose_layouts_lazy_vertical_grid_full_span] + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 30.dp) + ) { + item(span = { + // LazyGridItemSpanScope: + // maxLineSpan + GridItemSpan(maxLineSpan) + }) { + CategoryCard("Fruits") + } + // ... + } + // [END android_compose_layouts_lazy_vertical_grid_full_span] +} +@Composable +private fun CategoryCard(category: String) { +} +@Composable +private fun ListsSnippetsContentPadding() { + // [START android_compose_layouts_lazy_column_padding] + LazyColumn( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + ) { + // ... + } + // [END android_compose_layouts_lazy_column_padding] +} + +@Composable +private fun ListsSnippetsContentSpacing1() { + // [START android_compose_layouts_lazy_content_spacing] + LazyColumn( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + // ... + } + // [END android_compose_layouts_lazy_content_spacing] +} + +@Composable +private fun ListsSnippetsContentSpacing2() { + // [START android_compose_layouts_lazy_row_content_spacing] + LazyRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + // ... + } + // [END android_compose_layouts_lazy_row_content_spacing] +} + +@Composable +private fun ListsSnippetsContentSpacing3(photos: List) { + // [START android_compose_layouts_lazy_grid_content_spacing] + LazyVerticalGrid( + columns = GridCells.Fixed(2), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(photos) { item -> + PhotoItem(item) + } + } + // [END android_compose_layouts_lazy_grid_content_spacing] +} + +private object ListsSnippetsStickyHeaders1 { + // [START android_compose_layouts_lazy_column_sticky_header] + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun ListWithHeader(items: List) { + LazyColumn { + stickyHeader { + Header() + } + + items(items) { item -> + ItemRow(item) + } + } + } + // [END android_compose_layouts_lazy_column_sticky_header] +} + +private object ListsSnippetsStickyHeaders2 { + // [START android_compose_layouts_lazy_column_sticky_header_multiple] + // This ideally would be done in the ViewModel + val grouped = contacts.groupBy { it.firstName[0] } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun ContactsList(grouped: Map>) { + LazyColumn { + grouped.forEach { (initial, contactsForInitial) -> + stickyHeader { + CharacterHeader(initial) + } + + items(contactsForInitial) { contact -> + ContactListItem(contact) + } + } + } + } + // [END android_compose_layouts_lazy_column_sticky_header_multiple] +} + +private object ListsSnippetsGrids { + @Composable + fun PhotoGrid(photos: List) { + // [START android_compose_layouts_lazy_grid_adaptive] + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 128.dp) + ) { + items(photos) { photo -> + PhotoItem(photo) + } + } + // [END android_compose_layouts_lazy_grid_adaptive] + } +} + +private object ListsSnippetsReactingScrollPosition1 { + // [START android_compose_layouts_lazy_column_state] + @Composable + fun MessageList(messages: List) { + // Remember our own LazyListState + val listState = rememberLazyListState() + + // Provide it to LazyColumn + LazyColumn(state = listState) { + // ... + } + } + // [END android_compose_layouts_lazy_column_state] +} + +private object ListsSnippetsReactingScrollPosition2 { + // [START android_compose_layouts_lazy_column_scroll_to_top] + @Composable + fun MessageList(messages: List) { + Box { + val listState = rememberLazyListState() + + LazyColumn(state = listState) { + // ... + } + + // Show the button if the first visible item is past + // the first item. We use a remembered derived state to + // minimize unnecessary compositions + val showButton by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 0 + } + } + + AnimatedVisibility(visible = showButton) { + ScrollToTopButton() + } + } + } + // [END android_compose_layouts_lazy_column_scroll_to_top] +} + +@Composable +private fun ListsSnippetsReactingScrollPosition3(messages: List) { + // [START android_compose_layouts_lazy_column_state_react_event] + val listState = rememberLazyListState() + + LazyColumn(state = listState) { + // ... + } + + LaunchedEffect(listState) { + snapshotFlow { listState.firstVisibleItemIndex } + .map { index -> index > 0 } + .distinctUntilChanged() + .filter { it } + .collect { + MyAnalyticsService.sendScrolledPastFirstItemEvent() + } + } + // [END android_compose_layouts_lazy_column_state_react_event] +} + +private object ListsSnippetsControllingScrollPosition { + // [START android_compose_layouts_lazy_column_animate_scroll_to_item] + @Composable + fun MessageList(messages: List) { + val listState = rememberLazyListState() + // Remember a CoroutineScope to be able to launch + val coroutineScope = rememberCoroutineScope() + + LazyColumn(state = listState) { + // ... + } + + ScrollToTopButton( + onClick = { + coroutineScope.launch { + // Animate scroll to the first item + listState.animateScrollToItem(index = 0) + } + } + ) + } + // [END android_compose_layouts_lazy_column_animate_scroll_to_item] +} + +private object ListsSnippetsPaging { + // [START android_compose_layouts_lazy_column_paging] + @Composable + fun MessageList(pager: Pager) { + val lazyPagingItems = pager.flow.collectAsLazyPagingItems() + + LazyColumn { + items( + lazyPagingItems.itemCount, + key = lazyPagingItems.itemKey { it.id } + ) { index -> + val message = lazyPagingItems[index] + if (message != null) { + MessageRow(message) + } else { + MessagePlaceholder() + } + } + } + } + // [END android_compose_layouts_lazy_column_paging] +} + +private object ListsSnippetsItemKeys { + @Composable + fun MessageList(messages: List) { + // [START android_compose_layouts_lazy_column_item_keys] + LazyColumn { + items( + items = messages, + key = { message -> + // Return a stable + unique key for the item + message.id + } + ) { message -> + MessageRow(message) + } + } + // [END android_compose_layouts_lazy_column_item_keys] + } +} +data class Book(val id: String) +@Composable +private fun LazyColumnRemembered() { + val books = remember { + listOf(Book("1")) + } + // [START android_compose_layouts_lazy_column_remembered_value] + LazyColumn { + items(books, key = { it.id }) { + val rememberedValue = remember { + Random.nextInt() + } + } + } + // [END android_compose_layouts_lazy_column_remembered_value] +} + +@Composable +private fun LazyColumnAnyKey() { + val books = remember { + listOf(Book("1")) + } + // [START android_compose_layouts_lazy_column_any_key] + LazyColumn { + items(books, key = { + // primitives, enums, Parcelable, etc. + }) { + // ... + } + } + // [END android_compose_layouts_lazy_column_any_key] +} + +@Composable +private fun LazyColumnRememberSaveable() { + val books = remember { + listOf(Book("1")) + } + // [START android_compose_layouts_lazy_column_any_key_saveable] + LazyColumn { + items(books, key = { it.id }) { + val rememberedValue = rememberSaveable { + Random.nextInt() + } + } + } + // [END android_compose_layouts_lazy_column_any_key_saveable] +} + +@Composable +private fun LazyItemAnimations() { + val books = remember { + listOf(Book("1")) + } + // [START android_compose_layouts_lazy_column_item_animation] + LazyColumn { + // It is important to provide a key to each item to ensure animateItem() works as expected. + items(books, key = { it.id }) { + Row(Modifier.animateItem()) { + // ... + } + } + } + // [END android_compose_layouts_lazy_column_item_animation] +} + +@Composable +private fun LazyItemAnimationWithSpec() { + val books = remember { + listOf(Book("1")) + } + // [START android_compose_layouts_lazy_column_item_animation_with_spec] + LazyColumn { + items(books, key = { it.id }) { + Row( + Modifier.animateItem( + fadeInSpec = tween(durationMillis = 250), + fadeOutSpec = tween(durationMillis = 100), + placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy) + ) + ) { + // ... + } + } + } + // [END android_compose_layouts_lazy_column_item_animation_with_spec] +} + +private object LazyListTipSnippets { + + // [START android_compose_layouts_lazy_item_0_size] + @Composable + fun Item(imageUrl: String) { + AsyncImage( + model = rememberAsyncImagePainter(model = imageUrl), + modifier = Modifier.size(30.dp), + contentDescription = null + // ... + ) + } + // [END android_compose_layouts_lazy_item_0_size] + + @Composable + private fun NestingScrollableComponentsBroken() { + // DON'T DO THIS + val state = rememberScrollState() + // [START android_compose_layouts_lazy_item_nested_scrollable_dont] + // throws IllegalStateException + Column( + modifier = Modifier.verticalScroll(state) + ) { + LazyColumn { + // ... + } + } + // [END android_compose_layouts_lazy_item_nested_scrollable_dont] + } + @Composable + private fun NestingScrollableComponents() { + val data = remember { + listOf(Photo()) + } + // [START android_compose_layouts_lazy_item_nested_scrollable] + LazyColumn { + item { + Header() + } + items(data) { item -> + PhotoItem(item) + } + item { + Footer() + } + } + // [END android_compose_layouts_lazy_item_nested_scrollable] + } + + @Composable + private fun NestedRowColumn() { + val scrollState = rememberScrollState() + // This is allowed + // [START android_compose_layouts_lazy_item_nested_row_column] + Row( + modifier = Modifier.horizontalScroll(scrollState) + ) { + LazyColumn { + // ... + } + } + // [END android_compose_layouts_lazy_item_nested_row_column] + } + + @Composable + private fun NestedLazyColumnFixedSize() { + val scrollState = rememberScrollState() + // This is allowed + // [START android_compose_layouts_lazy_item_nested_lazy_column_fixed_size] + Column( + modifier = Modifier.verticalScroll(scrollState) + ) { + LazyColumn( + modifier = Modifier.height(200.dp) + ) { + // ... + } + } + // [END android_compose_layouts_lazy_item_nested_lazy_column_fixed_size] + } + + @Composable + private fun Footer() { + } +} + +private object LazyGridTipSnippets { + @Composable + private fun LazyGridMultipleElements_Avoid() { + // Avoid combining items that are logically different items, as they are handled as one + // entity now, it can hurt performance. It'll also interfere with methods such as + // scrollToItem() + // [START android_compose_layouts_lazy_grid_multiple_elements] + LazyVerticalGrid( + columns = GridCells.Adaptive(100.dp) + ) { + item { Item(0) } + item { + Item(1) + Item(2) + } + item { Item(3) } + // ... + } + // [END android_compose_layouts_lazy_grid_multiple_elements] + } + + @Composable + private fun LazyGridMultipleElementsDivider() { + // Valid use case for putting two composables together, when they are logically part of + // the same item. + // [START android_compose_layouts_lazy_grid_multiple_elements_divider] + LazyVerticalGrid( + columns = GridCells.Adaptive(100.dp) + ) { + item { Item(0) } + item { + Item(1) + Divider() + } + item { Item(2) } + // ... + } + // [END android_compose_layouts_lazy_grid_multiple_elements_divider] + } + @Composable + private fun Item(id: Int) { + } + @Composable + private fun Divider() { + } +} + +private object CustomArrangements { + // [START android_compose_layouts_lazy_custom_vertical_arrangement] + object TopWithFooter : Arrangement.Vertical { + override fun Density.arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) { + var y = 0 + sizes.forEachIndexed { index, size -> + outPositions[index] = y + y += size + } + if (y < totalSize) { + val lastIndex = outPositions.lastIndex + outPositions[lastIndex] = totalSize - sizes.last() + } + } + } + // [END android_compose_layouts_lazy_custom_vertical_arrangement] +} + +@Composable +private fun ContentTypeExample() { + // [START android_compose_layouts_lazy_content_type] + LazyColumn { + items(elements, contentType = { it.type }) { + // ... + } + } + // [END android_compose_layouts_lazy_content_type] +} + +@Preview +@Composable +fun LazyStaggeredGridSnippet() { + // [START android_compose_layouts_lazy_staggered_grid_adaptive] + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(200.dp), + verticalItemSpacing = 4.dp, + horizontalArrangement = Arrangement.spacedBy(4.dp), + content = { + items(randomSizedPhotos) { photo -> + AsyncImage( + model = photo, + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) + } + }, + modifier = Modifier.fillMaxSize() + ) + // [END android_compose_layouts_lazy_staggered_grid_adaptive] +} +@Preview +@Composable +fun LazyStaggeredGridSnippetFixed() { + // [START android_compose_layouts_lazy_staggered_grid_fixed] + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Fixed(3), + verticalItemSpacing = 4.dp, + horizontalArrangement = Arrangement.spacedBy(4.dp), + content = { + items(randomSizedPhotos) { photo -> + AsyncImage( + model = photo, + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) + } + }, + modifier = Modifier.fillMaxSize() + ) + // [END android_compose_layouts_lazy_staggered_grid_fixed] +} +class Message(val id: Long, val sender: String, val text: String) +private class Item + +private data class Contact(val firstName: String) +private val contacts = listOf() + +private class Photo +private val photos = listOf() + +@Composable +private fun MessageRow(message: Message) = Unit + +@Composable +private fun MessagePlaceholder() = Unit + +@Composable +private fun ItemRow(item: Item) = Unit + +@Composable +private fun Header() = Unit + +@Composable +private fun CharacterHeader(initial: Char) = Unit + +@Composable +private fun ContactListItem(contact: Contact) = Unit + +@Composable +private fun PhotoItem(photo: Photo) = Unit + +@Composable +private fun ScrollToTopButton(onClick: () -> Unit = {}) = Unit + +private object MyAnalyticsService { + fun sendScrolledPastFirstItemEvent() = Unit +} + +private class ContentTypeElement(val type: Long) +private val elements = listOf() + +private val randomSizedPhotos = listOf( + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 900, height = 1600), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 900, height = 1600), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 900, height = 1600), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 900, height = 1600), + randomSampleImageUrl(width = 500, height = 500), + randomSampleImageUrl(width = 300, height = 400), + randomSampleImageUrl(width = 1600, height = 900), + randomSampleImageUrl(width = 500, height = 500), +) +// [START android_compose_layouts_lazily_load_list] +@Composable +fun MessageList( + modifier: Modifier, + pager: Pager +) { + val lazyPagingItems = pager.flow.collectAsLazyPagingItems() + + LazyColumn { + items( + lazyPagingItems.itemCount, + key = lazyPagingItems.itemKey { it.id } + ) { index -> + val message = lazyPagingItems[index] + if (message != null) { + MessageRow(message) + } else { + MessagePlaceholder() + } + } + } + @Composable + fun MessagePlaceholder(modifier: Modifier) { + Box( + Modifier + .fillMaxWidth() + .height(48.dp) + ) { + CircularProgressIndicator() + } + } + + @Composable + fun MessageRow( + modifier: Modifier, + message: Message + ) { + Card(modifier = Modifier.padding(8.dp)) { + Column( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.Center + ) { + Text(message.sender) + Text(message.text) + } + } + } +} +// [END android_compose_layouts_lazily_load_list] + +// [START android_compose_lists_snap_scroll_button] +@Composable +fun MessageList(modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() + + LazyColumn(state = listState, modifier = Modifier.height(120.dp)) { + items(10) { index -> + Text( + modifier = Modifier.height(40.dp), + text = "Item $index" + ) + } + } + + Button(onClick = { + coroutineScope.launch { + listState.animateScrollToItem(index = 0) + } + }) { + Text(text = "Go top") + } +} +// [END android_compose_lists_snap_scroll_button] + +// [START android_compose_layout_scrollable_grid] +@Composable +fun ScrollingGrid() { + val itemsList = (0..15).toList() + + val itemModifier = Modifier + .border(1.dp, Color.Blue) + .width(80.dp) + .wrapContentSize() + + LazyHorizontalGrid( + rows = GridCells.Fixed(3), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(itemsList) { + Text("Item is $it", itemModifier) + } + + item { + Text("Single item", itemModifier) + } + } +} +// [END android_compose_layout_scrollable_grid] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/lists/ListWithMultipleItemTypes.kt b/compose/snippets/src/main/java/com/example/compose/snippets/lists/ListWithMultipleItemTypes.kt new file mode 100644 index 000000000..6e0fe575f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/lists/ListWithMultipleItemTypes.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.lists + +import android.provider.MediaStore +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import org.w3c.dom.Text + +// [START android_compose_lists_multiple_item_types] +@Composable +fun ListWithMultipleItems(messages: List) { + LazyColumn { + items( + messages.size, + contentType = { it } + ) { + for (message in messages) + when (message) { + is MediaStore.Audio -> AudioMessage(message) + is Text -> TextMessage(message) + } + } + } +} + +@Composable +fun AudioMessage(message: MediaStore.Audio) { + TODO("Not yet implemented.") +} + +@Composable +fun TextMessage(message: Text) { + TODO("Not yet implemented.") +} + +data class SampleMessage(val text: String, val content: Any) +// [END android_compose_lists_multiple_item_types] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/lists/NestedScrollingItem.kt b/compose/snippets/src/main/java/com/example/compose/snippets/lists/NestedScrollingItem.kt new file mode 100644 index 000000000..406b91f9a --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/lists/NestedScrollingItem.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.lists + +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage + +// [START android_compose_layouts_nested_scrolling] +@Composable +fun NestedScrollingRowsList(urls: List) { + LazyColumn { + items(10) { + LazyRow { + item { Text("Row: $it") } + items(urls.size) { index -> + // AsyncImage provided by Coil. + AsyncImage( + model = urls[index], + modifier = Modifier.size(150.dp), + contentDescription = null + ) + } + } + } + } +} +// [END android_compose_layouts_nested_scrolling] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/mentalmodel/ThinkingInComposeSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/mentalmodel/ThinkingInComposeSnippets.kt new file mode 100644 index 000000000..9c029ccda --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/mentalmodel/ThinkingInComposeSnippets.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.mentalmodel + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +// [START android_compose_thinking_in_compose_dynamic_content] +@Composable +fun Greeting(names: List) { + for (name in names) { + Text("Hello $name") + } +} +// [END android_compose_thinking_in_compose_dynamic_content] + +// [START android_compose_thinking_in_compose_recomposition] +@Composable +fun ClickCounter(clicks: Int, onClick: () -> Unit) { + Button(onClick = onClick) { + Text("I've been clicked $clicks times") + } +} +// [END android_compose_thinking_in_compose_recomposition] + +// [START android_compose_thinking_in_compose_recomposition_logic] +@Composable +fun SharedPrefsToggle( + text: String, + value: Boolean, + onValueChanged: (Boolean) -> Unit +) { + Row { + Text(text) + Checkbox(checked = value, onCheckedChange = onValueChanged) + } +} +// [END android_compose_thinking_in_compose_recomposition_logic] + +@Suppress("UNUSED_PARAMETER") +@Composable +fun MyFancyNavigation(content: @Composable () -> Unit) { +} + +@Composable +fun StartScreen() { +} + +@Composable +fun MiddleScreen() { +} + +@Composable +fun EndScreen() { +} + +// [START android_compose_thinking_in_compose_order] +@Composable +fun ButtonRow() { + MyFancyNavigation { + StartScreen() + MiddleScreen() + EndScreen() + } +} +// [END android_compose_thinking_in_compose_order] + +// [START android_compose_thinking_in_compose_parallel] +@Composable +fun ListComposable(myList: List) { + Row(horizontalArrangement = Arrangement.SpaceBetween) { + Column { + for (item in myList) { + Text("Item: $item") + } + } + Text("Count: ${myList.size}") + } +} +// [END android_compose_thinking_in_compose_parallel] + +// [START android_compose_thinking_in_compose_incorrect] +@Composable +fun ListWithBug(myList: List) { + var items = 0 + + Row(horizontalArrangement = Arrangement.SpaceBetween) { + Column { + for (item in myList) { + Card { + Text("Item: $item") + items++ // Avoid! Side-effect of the column recomposing. + } + } + } + Text("Count: $items") + } +} +// [END android_compose_thinking_in_compose_incorrect] + +// [START android_compose_thinking_in_compose_skips] +/** + * Display a list of names the user can click with a header + */ +@Composable +fun NamePicker( + header: String, + names: List, + onNameClicked: (String) -> Unit +) { + Column { + // this will recompose when [header] changes, but not when [names] changes + Text(header, style = MaterialTheme.typography.bodyLarge) + HorizontalDivider() + + // LazyColumn is the Compose version of a RecyclerView. + // The lambda passed to items() is similar to a RecyclerView.ViewHolder. + LazyColumn { + items(names) { name -> + // When an item's [name] updates, the adapter for that item + // will recompose. This will not recompose when [header] changes + NamePickerItem(name, onNameClicked) + } + } + } +} + +/** + * Display a single name the user can click. + */ +@Composable +private fun NamePickerItem(name: String, onClicked: (String) -> Unit) { + Text(name, Modifier.clickable(onClick = { onClicked(name) })) +} +// [END android_compose_thinking_in_compose_skips] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt new file mode 100644 index 000000000..665fea4b7 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/CustomModifierSnippets.kt @@ -0,0 +1,358 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.modifiers + +import android.annotation.SuppressLint +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween +import androidx.compose.animation.splineBasedDecay +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.ObserverModifierNode +import androidx.compose.ui.node.currentValueOf +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.node.invalidateMeasurement +import androidx.compose.ui.node.observeReads +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.constrain +import androidx.compose.ui.unit.constrainHeight +import androidx.compose.ui.unit.constrainWidth +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.offset +import kotlinx.coroutines.launch + +private object ClipModifierExample { + @SuppressLint("ModifierFactoryUnreferencedReceiver") // graphics layer does the reference + // [START android_compose_custom_modifiers_1] + fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true) + // [END android_compose_custom_modifiers_1] +} + +// [START android_compose_custom_modifiers_2] +fun Modifier.myBackground(color: Color) = padding(16.dp) + .clip(RoundedCornerShape(8.dp)) + .background(color) +// [END android_compose_custom_modifiers_2] + +// [START android_compose_custom_modifiers_3] +@Composable +fun Modifier.fade(enable: Boolean): Modifier { + val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f) + return this then Modifier.graphicsLayer { this.alpha = alpha } +} +// [END android_compose_custom_modifiers_3] + +// [START android_compose_custom_modifiers_4] +@Composable +fun Modifier.fadedBackground(): Modifier { + val color = LocalContentColor.current + return this then Modifier.background(color.copy(alpha = 0.5f)) +} +// [END android_compose_custom_modifiers_4] + +private object CustomModifierSnippets5 { + // [START android_compose_custom_modifiers_5] + @Composable + fun Modifier.myBackground(): Modifier { + val color = LocalContentColor.current + return this then Modifier.background(color.copy(alpha = 0.5f)) + } + + @Composable + fun MyScreen() { + CompositionLocalProvider(LocalContentColor provides Color.Green) { + // Background modifier created with green background + val backgroundModifier = Modifier.myBackground() + + // LocalContentColor updated to red + CompositionLocalProvider(LocalContentColor provides Color.Red) { + + // Box will have green background, not red as expected. + Box(modifier = backgroundModifier) + } + } + } + // [END android_compose_custom_modifiers_5] +} + +// [START android_compose_custom_modifiers_6] +val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations + +@Composable +fun Modifier.composableModifier(): Modifier { + val color = LocalContentColor.current.copy(alpha = 0.5f) + return this then Modifier.background(color) +} + +@Composable +fun MyComposable() { + val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher +} +// [END android_compose_custom_modifiers_6] + +// [START android_compose_custom_modifiers_7] +// Modifier.Node +private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { + override fun ContentDrawScope.draw() { + drawCircle(color) + } +} +// [END android_compose_custom_modifiers_7] + +// TODO: Create a new snippet that overrides InspectorInfo.inspectableProperties. +// [START android_compose_custom_modifiers_8] +// ModifierNodeElement +private data class CircleElement(val color: Color) : ModifierNodeElement() { + override fun create() = CircleNode(color) + + override fun update(node: CircleNode) { + node.color = color + } +} +// [END android_compose_custom_modifiers_8] + +// [START android_compose_custom_modifiers_9] +// Modifier factory +fun Modifier.circle(color: Color) = this then CircleElement(color) +// [END android_compose_custom_modifiers_9] + +private object CustomModifierSnippets10 { + // TODO: Create a new snippet that overrides InspectorInfo.inspectableProperties. + // [START android_compose_custom_modifiers_10] + // Modifier factory + fun Modifier.circle(color: Color) = this then CircleElement(color) + + // ModifierNodeElement + private data class CircleElement(val color: Color) : ModifierNodeElement() { + override fun create() = CircleNode(color) + + override fun update(node: CircleNode) { + node.color = color + } + } + + // Modifier.Node + private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { + override fun ContentDrawScope.draw() { + drawCircle(color) + } + } + // [END android_compose_custom_modifiers_10] +} + +// TODO: Create a new snippet that overrides InspectorInfo.inspectableProperties. +// [START android_compose_custom_modifiers_11] +fun Modifier.fixedPadding() = this then FixedPaddingElement + +data object FixedPaddingElement : ModifierNodeElement() { + override fun create() = FixedPaddingNode() + override fun update(node: FixedPaddingNode) {} +} + +class FixedPaddingNode : LayoutModifierNode, Modifier.Node() { + private val PADDING = 16.dp + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + val paddingPx = PADDING.roundToPx() + val horizontal = paddingPx * 2 + val vertical = paddingPx * 2 + + val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) + + val width = constraints.constrainWidth(placeable.width + horizontal) + val height = constraints.constrainHeight(placeable.height + vertical) + return layout(width, height) { + placeable.place(paddingPx, paddingPx) + } + } +} +// [END android_compose_custom_modifiers_11] + +// [START android_compose_custom_modifiers_12] +class BackgroundColorConsumerNode : + Modifier.Node(), + DrawModifierNode, + CompositionLocalConsumerModifierNode { + override fun ContentDrawScope.draw() { + val currentColor = currentValueOf(LocalContentColor) + drawRect(color = currentColor) + drawContent() + } +} +// [END android_compose_custom_modifiers_12] + +private object UnityDensity : Density { + override val density: Float + get() = 1f + override val fontScale: Float + get() = 1f +} +data class DefaultFlingBehavior(var flingDecay: DecayAnimationSpec) +// [START android_compose_custom_modifiers_13] +class ScrollableNode : + Modifier.Node(), + ObserverModifierNode, + CompositionLocalConsumerModifierNode { + + // Place holder fling behavior, we'll initialize it when the density is available. + val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity)) + + override fun onAttach() { + updateDefaultFlingBehavior() + observeReads { currentValueOf(LocalDensity) } // monitor change in Density + } + + override fun onObservedReadsChanged() { + // if density changes, update the default fling behavior. + updateDefaultFlingBehavior() + } + + private fun updateDefaultFlingBehavior() { + val density = currentValueOf(LocalDensity) + defaultFlingBehavior.flingDecay = splineBasedDecay(density) + } +} +// [END android_compose_custom_modifiers_13] + +object CustomModifierSnippets14 { + // [START android_compose_custom_modifiers_14] + class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { + private lateinit var alpha: Animatable + + override fun ContentDrawScope.draw() { + drawCircle(color = color, alpha = alpha.value) + drawContent() + } + + override fun onAttach() { + alpha = Animatable(1f) + coroutineScope.launch { + alpha.animateTo( + 0f, + infiniteRepeatable(tween(1000), RepeatMode.Reverse) + ) { + } + } + } + } + // [END android_compose_custom_modifiers_14] +} + +class InteractionData +class FocusableNode(val interactionData: InteractionData) : DelegatableNode { + override val node: Modifier.Node + get() = TODO("Not yet implemented") +} +class IndicationNode(val interactionData: InteractionData) : DelegatableNode { + override val node: Modifier.Node + get() = TODO("Not yet implemented") +} +// [START android_compose_custom_modifiers_15] +class ClickableNode : DelegatingNode() { + val interactionData = InteractionData() + val focusableNode = delegate( + FocusableNode(interactionData) + ) + val indicationNode = delegate( + IndicationNode(interactionData) + ) +} +// [END android_compose_custom_modifiers_15] + +class ClickablePointerInputNode(var onClick: () -> Unit) : Modifier.Node(), DelegatableNode { + fun update(onClick: () -> Unit) { + this.onClick = onClick + } +} +// [START android_compose_custom_modifiers_16] +class SampleInvalidatingNode( + var color: Color, + var size: IntSize, + var onClick: () -> Unit +) : DelegatingNode(), LayoutModifierNode, DrawModifierNode { + override val shouldAutoInvalidate: Boolean + get() = false + + private val clickableNode = delegate( + ClickablePointerInputNode(onClick) + ) + + fun update(color: Color, size: IntSize, onClick: () -> Unit) { + if (this.color != color) { + this.color = color + // Only invalidate draw when color changes + invalidateDraw() + } + + if (this.size != size) { + this.size = size + // Only invalidate layout when size changes + invalidateMeasurement() + } + + // If only onClick changes, we don't need to invalidate anything + clickableNode.update(onClick) + } + + override fun ContentDrawScope.draw() { + drawRect(color) + } + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + val size = constraints.constrain(size) + val placeable = measurable.measure(constraints) + return layout(size.width, size.height) { + placeable.place(0, 0) + } + } +} +// [END android_compose_custom_modifiers_16] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/ModifierSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/ModifierSnippets.kt new file mode 100644 index 000000000..efd1403f0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/ModifierSnippets.kt @@ -0,0 +1,434 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.modifiers + +import androidx.compose.animation.core.AnimationState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +private object Padding { + // [START android_compose_modifiers_padding] + @Composable + private fun Greeting(name: String) { + Column(modifier = Modifier.padding(24.dp)) { + Text(text = "Hello,") + Text(text = name) + } + } + // [END android_compose_modifiers_padding] +} + +private object PaddingAndFill { + // [START android_compose_modifiers_paddingandfill] + @Composable + private fun Greeting(name: String) { + Column( + modifier = Modifier + .padding(24.dp) + .fillMaxWidth() + ) { + Text(text = "Hello,") + Text(text = name) + } + } + // [END android_compose_modifiers_paddingandfill] +} + +private object Order1 { + val onClick = {} + + // [START android_compose_modifiers_order1] + @Composable + fun ArtistCard(/*...*/) { + val padding = 16.dp + Column( + Modifier + .clickable(onClick = onClick) + .padding(padding) + .fillMaxWidth() + ) { + // rest of the implementation + } + } + // [END android_compose_modifiers_order1] +} + +private object Order2 { + val onClick = {} + + // [START android_compose_modifiers_order2] + @Composable + fun ArtistCard(/*...*/) { + val padding = 16.dp + Column( + Modifier + .padding(padding) + .clickable(onClick = onClick) + .fillMaxWidth() + ) { + // rest of the implementation + } + } + // [END android_compose_modifiers_order2] +} + +private object Size { + // [START android_compose_modifiers_size] + @Composable + fun ArtistCard(/*...*/) { + Row( + modifier = Modifier.size(width = 400.dp, height = 100.dp) + ) { + Image(/*...*/) + Column { /*...*/ } + } + } + // [END android_compose_modifiers_size] +} + +private object RequiredSize { + // [START android_compose_modifiers_requiredsize] + @Composable + fun ArtistCard(/*...*/) { + Row( + modifier = Modifier.size(width = 400.dp, height = 100.dp) + ) { + Image( + /*...*/ + modifier = Modifier.requiredSize(150.dp) + ) + Column { /*...*/ } + } + } + // [END android_compose_modifiers_requiredsize] +} + +private object FillMaxHeight { + // [START android_compose_modifiers_fillmaxheight] + @Composable + fun ArtistCard(/*...*/) { + Row( + modifier = Modifier.size(width = 400.dp, height = 100.dp) + ) { + Image( + /*...*/ + modifier = Modifier.fillMaxHeight() + ) + Column { /*...*/ } + } + } + // [END android_compose_modifiers_fillmaxheight] +} + +private object PaddingFromBaseline { + // [START android_compose_modifiers_paddingfrombaseline] + @Composable + fun ArtistCard(artist: Artist) { + Row(/*...*/) { + Column { + Text( + text = artist.name, + modifier = Modifier.paddingFromBaseline(top = 50.dp) + ) + Text(artist.lastSeenOnline) + } + } + } + // [END android_compose_modifiers_paddingfrombaseline] +} + +private object Offset { + // [START android_compose_modifiers_offset] + @Composable + fun ArtistCard(artist: Artist) { + Row(/*...*/) { + Column { + Text(artist.name) + Text( + text = artist.lastSeenOnline, + modifier = Modifier.offset(x = 4.dp) + ) + } + } + } + // [END android_compose_modifiers_offset] +} + +private object MatchParentSize { + // [START android_compose_modifiers_matchparentsize] + @Composable + fun MatchParentSizeComposable() { + Box { + Spacer( + Modifier + .matchParentSize() + .background(Color.LightGray) + ) + ArtistCard() + } + } + // [END android_compose_modifiers_matchparentsize] +} + +private object Weight { + // [START android_compose_modifiers_weight] + @Composable + fun ArtistCard(/*...*/) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + Image( + /*...*/ + modifier = Modifier.weight(2f) + ) + Column( + modifier = Modifier.weight(1f) + ) { + /*...*/ + } + } + } + // [END android_compose_modifiers_weight] +} + +private object ReusingModifiers { + // [START android_compose_modifiers_reusingmodifiers] + val reusableModifier = Modifier + .fillMaxWidth() + .background(Color.Red) + .padding(12.dp) + // [END android_compose_modifiers_reusingmodifiers] +} + +private object AnimationInline { + // [START android_compose_modifiers_animationinline] + @Composable + fun LoadingWheelAnimation() { + val animatedState = animateFloatAsState(/*...*/) + + LoadingWheel( + // Creation and allocation of this modifier will happen on every frame of the animation! + modifier = Modifier + .padding(12.dp) + .background(Color.Gray), + animatedState = animatedState + ) + } + // [END android_compose_modifiers_animationinline] +} + +private object AnimationExtracted { + // [START android_compose_modifiers_animationextracted] + // Now, the allocation of the modifier happens here: + val reusableModifier = Modifier + .padding(12.dp) + .background(Color.Gray) + + @Composable + fun LoadingWheelAnimation() { + val animatedState = animateFloatAsState(/*...*/) + + LoadingWheel( + // No allocation, as we're just reusing the same instance + modifier = reusableModifier, + animatedState = animatedState + ) + } + // [END android_compose_modifiers_animationextracted] +} + +private object UnscopedModifiers1 { + // [START android_compose_modifiers_unscopedmodifiers1] + val reusableModifier = Modifier + .fillMaxWidth() + .background(Color.Red) + .padding(12.dp) + + @Composable + fun AuthorField() { + HeaderText( + // ... + modifier = reusableModifier + ) + SubtitleText( + // ... + modifier = reusableModifier + ) + } + // [END android_compose_modifiers_unscopedmodifiers1] +} + +private object UnscopedModifiers2 { + // [START android_compose_modifiers_unscopedmodifiers2] + val reusableItemModifier = Modifier + .padding(bottom = 12.dp) + .size(216.dp) + .clip(CircleShape) + + @Composable + private fun AuthorList(authors: List) { + LazyColumn { + items(authors) { + AsyncImage( + // ... + modifier = reusableItemModifier, + ) + } + } + } + // [END android_compose_modifiers_unscopedmodifiers2] +} + +@Composable +private fun ScopedModifiers1() { + // [START android_compose_modifiers_scopedmodifiers1] + Column(/*...*/) { + val reusableItemModifier = Modifier + .padding(bottom = 12.dp) + // Align Modifier.Element requires a ColumnScope + .align(Alignment.CenterHorizontally) + .weight(1f) + Text1( + modifier = reusableItemModifier, + // ... + ) + Text2( + modifier = reusableItemModifier + // ... + ) + // ... + } + // [END android_compose_modifiers_scopedmodifiers1] +} + +@Composable +private fun ScopedModifiers2() { + // [START android_compose_modifiers_scopedmodifiers2] + Column(modifier = Modifier.fillMaxWidth()) { + // Weight modifier is scoped to the Column composable + val reusableItemModifier = Modifier.weight(1f) + + // Weight will be properly assigned here since this Text is a direct child of Column + Text1( + modifier = reusableItemModifier + // ... + ) + + Box { + Text2( + // Weight won't do anything here since the Text composable is not a direct child of Column + modifier = reusableItemModifier + // ... + ) + } + } + // [END android_compose_modifiers_scopedmodifiers2] +} + +private object ChainingExtractedModifiers { + val otherModifier = Modifier + + @Composable + private fun Snippet() { + // Note: In a real app, you would define the modifier outside of the composable where it is + // needed. For the snippet in the documentation, putting it in one place is clearer. + // [START android_compose_modifiers_chainingextractedmodifiers] + val reusableModifier = Modifier + .fillMaxWidth() + .background(Color.Red) + .padding(12.dp) + + // Append to your reusableModifier + reusableModifier.clickable { /*...*/ } + + // Append your reusableModifier + otherModifier.then(reusableModifier) + // [END android_compose_modifiers_chainingextractedmodifiers] + } +} + +// ----- Classes and functions below are to simplify snippets above ----- // + +private data class Artist(val name: String, val lastSeenOnline: String) +private data class Author(val name: String) + +@Composable +private fun Image(modifier: Modifier = Modifier) { + // Empty to allow Image(/*...*/) in snippets +} + +@Composable +private fun ArtistCard() { + // Empty for snippets +} + +@Composable +private fun LoadingWheel(modifier: Modifier, animatedState: State) { + // Empty for snippets +} + +@Composable +private fun animateFloatAsState(): State { + return AnimationState(initialValue = 0f) +} + +@Composable +private fun HeaderText(modifier: Modifier = Modifier) { + // Empty for snippets +} + +@Composable +private fun SubtitleText(modifier: Modifier = Modifier) { + // Empty for snippets +} + +@Composable +private fun AsyncImage(modifier: Modifier = Modifier) { + // Empty for snippets +} + +@Composable +private fun Text1(modifier: Modifier = Modifier) { + // Empty for snippets +} + +@Composable +private fun Text2(modifier: Modifier = Modifier) { + // Empty for snippets +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/VisibilitySnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/VisibilitySnippets.kt new file mode 100644 index 000000000..dbad0cc1b --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/modifiers/VisibilitySnippets.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.modifiers + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onFirstVisible +import androidx.compose.ui.layout.onVisibilityChanged +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp + +private object OnVisibilityChangedSample { + + @Composable + fun OnVisibilityChangedExample(modifier: Modifier = Modifier) { + // [START android_compose_modifiers_onVisibilityChanged] + Text( + text = "Some text", + modifier = Modifier + .onVisibilityChanged { visible -> + if (visible) { + // Do something if visible + } else { + // Do something if not visible + } + } + .padding(vertical = 8.dp) + ) + // [END android_compose_modifiers_onVisibilityChanged] + } +} + +private object OnVisibilityChangedMinFractionVisible { + + @Composable + fun OnVisibilityChangedModifierMinFractionExample(modifier: Modifier = Modifier) { + // [START android_compose_modifiers_onVisibilityChangedMinFraction] + LazyColumn( + modifier = modifier.fillMaxSize() + ) { + item { + Box( + modifier = Modifier + // [START_EXCLUDE] + .fillParentMaxWidth() + .border(0.5.dp, Color.Gray) + // [END_EXCLUDE] + // Here the visible callback gets triggered when 20% of the composable is visible + .onVisibilityChanged( + minFractionVisible = 0.2f, + ) { visible -> + if (visible) { + // Call specific logic here + // viewModel.fetchDataFromNetwork() + } + } + .padding(vertical = 16.dp) + ) { + Text( + text = "Sample Text", + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + } + // [END android_compose_modifiers_onVisibilityChangedMinFraction] + } +} + +private object onVisibilityChangedMinDuration { + + val MutedPlum = Color(0xFF7B4B6B) + val PalePink = Color(0xFFF3E9EB) + + @Composable + fun OnVisibilityChangedMinDurationExample( + @DrawableRes imageRes: Int, + modifier: Modifier = Modifier, + ) { + // [START android_compose_modifiers_onVisibilityChangedMinDuration] + var background by remember { mutableStateOf(PalePink) } + Card( + modifier = modifier + // [START_EXCLUDE] + .height(300.dp) + .fillMaxWidth() + // [END_EXCLUDE] + .onVisibilityChanged(minDurationMs = 3000) { + if (it) { + background = MutedPlum + } + } + ) { + + Box( + modifier = Modifier + // [START_EXCLUDE] + .fillMaxSize() + // [END_EXCLUDE] + .background(background), + contentAlignment = Alignment.Center, + ) { + // [START_EXCLUDE] + Image( + painter = painterResource(id = imageRes), + contentDescription = "Androidify Bot", + ) + // [END_EXCLUDE] + } + } + // [END android_compose_modifiers_onVisibilityChangedMinDuration] + } +} + + +private object OnFirstVisibleSample { + + @Composable + fun OnFirstVisibleExample(id: Int, modifier: Modifier = Modifier) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(8.dp) + ) { + // [START android_compose_modifiers_onFirstVisible] + Text( + modifier = Modifier + .fillMaxSize() + .onFirstVisible { + println("OnFirstVisible : ProductCard: $id is visible") + } + .padding(8.dp), + text = "Product $id" + ) + // [END android_compose_modifiers_onFirstVisible] + } + } +} \ No newline at end of file diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt new file mode 100644 index 000000000..8d86b28b4 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.navigation + +enum class Destination(val route: String, val title: String) { + BrushExamples("brushExamples", "Brush Examples"), + ImageExamples("imageExamples", "Image Examples"), + AnimationQuickGuideExamples("animationExamples", "Animation Examples"), + ComponentsExamples("topComponents", "Top Compose Components"), + ScreenshotExample("screenshotExample", "Screenshot Examples"), + ShapesExamples("shapesExamples", "Shapes Examples"), + SharedElementExamples("sharedElement", "Shared elements"), + PagerExamples("pagerExamples", "Pager examples") +} + +// Enum class for compose components navigation screen. +enum class TopComponentsDestination(val route: String, val title: String) { + CardExamples("cardExamples", "Card"), + SwitchExamples("switchExamples", "Switch"), + SliderExamples("sliderExamples", "Slider"), + DialogExamples("dialogExamples", "Dialog"), + ChipExamples("chipExamples", "Chip"), + FloatingActionButtonExamples("floatingActionButtonExamples", "Floating Action Button"), + ButtonExamples("buttonExamples", "Button"), + ProgressIndicatorExamples("progressIndicatorExamples", "Progress Indicators"), + ScaffoldExample("scaffoldExample", "Scaffold"), + AppBarExamples("appBarExamples", "App bars"), + CheckboxExamples("checkboxExamples", "Checkbox"), + DividerExamples("dividerExamples", "Dividers"), + BadgeExamples("badgeExamples", "Badges"), + PartialBottomSheet("partialBottomSheets", "Partial Bottom Sheet"), + TimePickerExamples("timePickerExamples", "Time Pickers"), + DatePickerExamples("datePickerExamples", "Date Pickers"), + CarouselExamples("carouselExamples", "Carousel"), + MenusExample("menusExamples", "Menus"), + TooltipExamples("tooltipExamples", "Tooltips"), + NavigationDrawerExamples("navigationDrawerExamples", "Navigation drawer"), + SegmentedButtonExamples("segmentedButtonExamples", "Segmented button"), + SwipeToDismissBoxExamples("swipeToDismissBoxExamples", "Swipe to dismiss box examples"), + SearchBarExamples("searchBarExamples", "Search bar") +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/performance/PerformanceSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/performance/PerformanceSnippets.kt new file mode 100644 index 000000000..fc9af5d63 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/performance/PerformanceSnippets.kt @@ -0,0 +1,307 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "UNUSED_PARAMETER", "UNUSED_VARIABLE") + +package com.example.compose.snippets.performance + +import android.annotation.SuppressLint +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset + +private object ContactsListBadSort { + // [START android_compose_performance_contactslistbadsort] + @Composable + fun ContactList( + contacts: List, + comparator: Comparator, + modifier: Modifier = Modifier + ) { + LazyColumn(modifier) { + // DON’T DO THIS + items(contacts.sortedWith(comparator)) { contact -> + // ... + } + } + } + // [END android_compose_performance_contactslistbadsort] +} + +private object ContactsListGoodSort { + // [START android_compose_performance_contactslistgoodsort] + @Composable + fun ContactList( + contacts: List, + comparator: Comparator, + modifier: Modifier = Modifier + ) { + val sortedContacts = remember(contacts, comparator) { + contacts.sortedWith(comparator) + } + + LazyColumn(modifier) { + items(sortedContacts) { + // ... + } + } + } + // [END android_compose_performance_contactslistgoodsort] +} + +private object NotesListNoKey { + // [START android_compose_performance_listnokey] + @Composable + fun NotesList(notes: List) { + LazyColumn { + items( + items = notes + ) { note -> + NoteRow(note) + } + } + } + // [END android_compose_performance_listnokey] +} + +private object NotesListKey { + // [START android_compose_performance_listkey] + @Composable + fun NotesList(notes: List) { + LazyColumn { + items( + items = notes, + key = { note -> + // Return a stable, unique key for the note + note.id + } + ) { note -> + NoteRow(note) + } + } + } + // [END android_compose_performance_listkey] +} + +private object RememberListBad { + @SuppressLint("FrequentlyChangedStateReadInComposition") + @Composable + fun RememberListStateBad() { + // [START android_compose_performance_rememberliststatebad] + val listState = rememberLazyListState() + + LazyColumn(state = listState) { + // ... + } + + val showButton = listState.firstVisibleItemIndex > 0 + + AnimatedVisibility(visible = showButton) { + ScrollToTopButton() + } + // [END android_compose_performance_rememberliststatebad] + } +} + +private object RememberListGood { + @Composable + fun RememberListGood() { + // [START android_compose_performance_rememberliststategood] + val listState = rememberLazyListState() + + LazyColumn(state = listState) { + // ... + } + + val showButton by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 0 + } + } + + AnimatedVisibility(visible = showButton) { + ScrollToTopButton() + } + // [END android_compose_performance_rememberliststategood] + } +} + +private object DeferredReadBefore { + // [START android_compose_performance_deferredreadbefore] + @Composable + fun SnackDetail() { + // ... + + Box(Modifier.fillMaxSize()) { // Recomposition Scope Start + val scroll = rememberScrollState(0) + // ... + Title(snack, scroll.value) + // ... + } // Recomposition Scope End + } + + @Composable + private fun Title(snack: Snack, scroll: Int) { + // ... + val offset = with(LocalDensity.current) { scroll.toDp() } + + Column( + modifier = Modifier + .offset(y = offset) + ) { + // ... + } + } + // [END android_compose_performance_deferredreadbefore] +} + +private object DeferredReadGood { + // [START android_compose_performance_deferredreadafter] + @Composable + fun SnackDetail() { + // ... + + Box(Modifier.fillMaxSize()) { // Recomposition Scope Start + val scroll = rememberScrollState(0) + // ... + Title(snack) { scroll.value } + // ... + } // Recomposition Scope End + } + + @Composable + private fun Title(snack: Snack, scrollProvider: () -> Int) { + // ... + val offset = with(LocalDensity.current) { scrollProvider().toDp() } + Column( + modifier = Modifier + .offset(y = offset) + ) { + // ... + } + } + // [END android_compose_performance_deferredreadafter] +} + +// [START android_compose_performance_deferredreadafter2] +@Composable +private fun Title(snack: Snack, scrollProvider: () -> Int) { + // ... + Column( + modifier = Modifier + .offset { IntOffset(x = 0, y = scrollProvider()) } + ) { + // ... + } +} +// [END android_compose_performance_deferredreadafter2] + +@Composable +fun AnimateColorBefore() { + // [START android_compose_performance_animatecolorbefore] + // Here, assume animateColorBetween() is a function that swaps between + // two colors + val color by animateColorBetween(Color.Cyan, Color.Magenta) + + Box( + Modifier + .fillMaxSize() + .background(color) + ) + // [END android_compose_performance_animatecolorbefore] +} + +@Composable +fun AnimateColorAfter() { +// [START android_compose_performance_animatecolorafter] + val color by animateColorBetween(Color.Cyan, Color.Magenta) + Box( + Modifier + .fillMaxSize() + .drawBehind { + drawRect(color) + } + ) +// [END android_compose_performance_animatecolorafter] +} + +private object BackwardsWrite { + // [START android_compose_performance_backwardswrite] + @Composable + fun BadComposable() { + var count by remember { mutableIntStateOf(0) } + + // Causes recomposition on click + Button(onClick = { count++ }, Modifier.wrapContentSize()) { + Text("Recompose") + } + + Text("$count") + count++ // Backwards write, writing to state after it has been read + } + // [END android_compose_performance_backwardswrite] +} + +/*** + * Fakes needed for snippets to build: + ***/ + +private data class Contact(val name: String) : Comparable { + override fun compareTo(other: Contact): Int { + TODO("Not yet implemented") + } +} + +class Note { + val id: Int = 0 +} + +class NoteRow(note: Any) + +@Composable +private fun ScrollToTopButton(onClick: () -> Unit = {}) = Unit + +val snack = Snack() + +class Snack + +@Composable +private fun animateColorBetween(color1: Color, color2: Color): State { + return remember { mutableStateOf(color1) } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/phases/PhasesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/phases/PhasesSnippets.kt new file mode 100644 index 000000000..1ca55b9e0 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/phases/PhasesSnippets.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.phases + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp + +@Composable +private fun StateReadWithoutPropertyDelegate() { + // [START android_compose_phases_state_read_without_property_delegate] + // State read without property delegate. + val paddingState: MutableState = remember { mutableStateOf(8.dp) } + Text( + text = "Hello", + modifier = Modifier.padding(paddingState.value) + ) + // [END android_compose_phases_state_read_without_property_delegate] +} + +@Suppress("CanBeVal") +@Composable +private fun StateReadWithPropertyDelegate() { + // [START android_compose_phases_state_read_with_property_delegate] + // State read with property delegate. + var padding: Dp by remember { mutableStateOf(8.dp) } + Text( + text = "Hello", + modifier = Modifier.padding(padding) + ) + // [END android_compose_phases_state_read_with_property_delegate] +} + +@Suppress("CanBeVal") +@Composable +private fun CompositionSnippet() { + // [START android_compose_phases_composition] + var padding by remember { mutableStateOf(8.dp) } + Text( + text = "Hello", + // The `padding` state is read in the composition phase + // when the modifier is constructed. + // Changes in `padding` will invoke recomposition. + modifier = Modifier.padding(padding) + ) + // [END android_compose_phases_composition] +} + +@Suppress("CanBeVal") +@Composable +private fun LayoutSnippet() { + // [START android_compose_phases_layout] + var offsetX by remember { mutableStateOf(8.dp) } + Text( + text = "Hello", + modifier = Modifier.offset { + // The `offsetX` state is read in the placement step + // of the layout phase when the offset is calculated. + // Changes in `offsetX` restart the layout. + IntOffset(offsetX.roundToPx(), 0) + } + ) + // [END android_compose_phases_layout] +} + +@Suppress("CanBeVal") +@Composable +private fun DrawingSnippet(modifier: Modifier) { + // [START android_compose_phases_drawing] + var color by remember { mutableStateOf(Color.Red) } + Canvas(modifier = modifier) { + // The `color` state is read in the drawing phase + // when the canvas is rendered. + // Changes in `color` restart the drawing. + drawRect(color) + } + // [END android_compose_phases_drawing] +} + +@Composable +private fun OptimizeStateReadsBefore() { + // [START android_compose_phases_optimize_state_reads_before] + Box { + val listState = rememberLazyListState() + + Image( + // [START_EXCLUDE] + painterResource(id = android.R.drawable.star_on), + contentDescription = null, + // [END_EXCLUDE] + // Non-optimal implementation! + Modifier.offset( + with(LocalDensity.current) { + // State read of firstVisibleItemScrollOffset in composition + (listState.firstVisibleItemScrollOffset / 2).toDp() + } + ) + ) + + LazyColumn(state = listState) { + // [START_EXCLUDE] + // [END_EXCLUDE] + } + } + // [END android_compose_phases_optimize_state_reads_before] +} + +@Composable +private fun OptimizeStateReadsAfter() { + // [START android_compose_phases_optimize_state_reads_after] + Box { + val listState = rememberLazyListState() + + Image( + // [START_EXCLUDE] + painterResource(id = android.R.drawable.star_on), + contentDescription = null, + // [END_EXCLUDE] + Modifier.offset { + // State read of firstVisibleItemScrollOffset in Layout + IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) + } + ) + + LazyColumn(state = listState) { + // [START_EXCLUDE] + // [END_EXCLUDE] + } + } + // [END android_compose_phases_optimize_state_reads_after] +} + +private object Loop { + @Suppress("ClassName") + object R { + object drawable { + const val rectangle = 1 + } + } + + @Composable + fun Loop() { + // [START android_compose_phases_loop] + Box { + var imageHeightPx by remember { mutableIntStateOf(0) } + + Image( + painter = painterResource(R.drawable.rectangle), + contentDescription = "I'm above the text", + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { size -> + // Don't do this + imageHeightPx = size.height + } + ) + + Text( + text = "I'm below the image", + modifier = Modifier.padding( + top = with(LocalDensity.current) { imageHeightPx.toDp() } + ) + ) + } + // [END android_compose_phases_loop] + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt new file mode 100644 index 000000000..295894832 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/pictureinpicture/PictureInPictureSnippets.kt @@ -0,0 +1,406 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.pictureinpicture + +import android.app.PictureInPictureParams +import android.app.RemoteAction +import android.content.BroadcastReceiver +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.util.Log +import android.util.Rational +import androidx.activity.ComponentActivity +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toAndroidRectF +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.content.ContextCompat +import androidx.core.graphics.toRect +import androidx.core.util.Consumer +import androidx.media3.common.Player +import androidx.media3.common.VideoSize +import androidx.media3.exoplayer.ExoPlayer + +var shouldEnterPipMode by mutableStateOf(false) +private const val PIP_TAG = "PiP info" + +// [START android_compose_pip_broadcast_receiver_constants] +// Constant for broadcast receiver +const val ACTION_BROADCAST_CONTROL = "broadcast_control" + +// Intent extras for broadcast controls from Picture-in-Picture mode. +const val EXTRA_CONTROL_TYPE = "control_type" +const val EXTRA_CONTROL_PLAY = 1 +const val EXTRA_CONTROL_PAUSE = 2 +// [END android_compose_pip_broadcast_receiver_constants] + +@Composable +fun PiPBuilderSetAutoEnterEnabled( + modifier: Modifier = Modifier +) { + val context = LocalContext.current + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // [START android_compose_pip_builder_auto_enter] + val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> + val builder = PictureInPictureParams.Builder() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(true) + } + context.findActivity().setPictureInPictureParams(builder.build()) + } + VideoPlayer(pipModifier) + // [END android_compose_pip_builder_auto_enter] + } else { + Log.i(PIP_TAG, "API does not support PiP") + } +} + +// [START android_compose_pip_find_activity] +internal fun Context.findActivity(): ComponentActivity { + var context = this + while (context is ContextWrapper) { + if (context is ComponentActivity) return context + context = context.baseContext + } + throw IllegalStateException("Picture in picture should be called in the context of an Activity") +} +// [END android_compose_pip_find_activity] + +@Composable +fun EnterPiPThroughButton() { + // [START android_compose_pip_button_click] + val context = LocalContext.current + Button(onClick = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.findActivity().enterPictureInPictureMode( + PictureInPictureParams.Builder().build() + ) + } else { + Log.i(PIP_TAG, "API does not support PiP") + } + }) { + Text(text = "Enter PiP mode!") + } + // [END android_compose_pip_button_click] +} + +// [START android_compose_pip_is_in_pip_mode] +@Composable +fun rememberIsInPipMode(): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val activity = LocalContext.current.findActivity() + var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) } + DisposableEffect(activity) { + val observer = Consumer { info -> + pipMode = info.isInPictureInPictureMode + } + activity.addOnPictureInPictureModeChangedListener( + observer + ) + onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } + } + return pipMode + } else { + return false + } +} +// [END android_compose_pip_is_in_pip_mode] + +@Composable +fun VideoPlayer() { +} +@Composable +fun VideoPlayer(modifier: Modifier) { +} + +@Composable +fun ToggleUIBasedOnPiP( + modifier: Modifier = Modifier, +) { + // [START android_compose_pip_ui_toggle] + val inPipMode = rememberIsInPipMode() + + Column(modifier = modifier) { + // This text will only show up when the app is not in PiP mode + if (!inPipMode) { + Text( + text = "Picture in Picture", + ) + } + VideoPlayer() + } + // [END android_compose_pip_ui_toggle] +} + +fun initializePlayer(context: Context) { + val player = ExoPlayer.Builder(context.applicationContext) + .build().apply {} + + // [START android_compose_pip_toggle_pip_on_if_video_is_playing] + player.addListener(object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + shouldEnterPipMode = isPlaying + } + }) + // [END android_compose_pip_toggle_pip_on_if_video_is_playing] +} + +// [START android_compose_pip_release_player] +fun releasePlayer() { + shouldEnterPipMode = false +} +// [END android_compose_pip_release_player] + +@Composable +fun PiPBuilderSetAutoEnterEnabledUsingState( + shouldEnterPipMode: Boolean, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // [START android_compose_pip_post_12_should_enter_pip] + val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> + val builder = PictureInPictureParams.Builder() + + // Add autoEnterEnabled for versions S and up + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(shouldEnterPipMode) + } + context.findActivity().setPictureInPictureParams(builder.build()) + } + + VideoPlayer(pipModifier) + // [END android_compose_pip_post_12_should_enter_pip] + } else { + Log.i(PIP_TAG, "API does not support PiP") + } +} + +@Composable +fun PiPBuilderSetSourceRect( + shouldEnterPipMode: Boolean, + modifier: Modifier = Modifier, +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // [START android_compose_pip_set_source_rect] + val context = LocalContext.current + + val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> + val builder = PictureInPictureParams.Builder() + if (shouldEnterPipMode) { + val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() + builder.setSourceRectHint(sourceRect) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(shouldEnterPipMode) + } + context.findActivity().setPictureInPictureParams(builder.build()) + } + + VideoPlayer(pipModifier) + // [END android_compose_pip_set_source_rect] + } else { + Log.i(PIP_TAG, "API does not support PiP") + } +} + +@Composable +fun PiPBuilderSetAspectRatio( + player: Player?, + shouldEnterPipMode: Boolean, + modifier: Modifier = Modifier, +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // [START android_compose_pip_set_aspect_ratio] + val context = LocalContext.current + + val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> + val builder = PictureInPictureParams.Builder() + if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { + val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() + builder.setSourceRectHint(sourceRect) + builder.setAspectRatio( + Rational(player.videoSize.width, player.videoSize.height) + ) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(shouldEnterPipMode) + } + context.findActivity().setPictureInPictureParams(builder.build()) + } + + VideoPlayer(pipModifier) + // [END android_compose_pip_set_aspect_ratio] + } else { + Log.i(PIP_TAG, "API does not support PiP") + } +} + +// [START android_compose_pip_broadcast_receiver] +@RequiresApi(Build.VERSION_CODES.O) +@Composable +fun PlayerBroadcastReceiver(player: Player?) { + val isInPipMode = rememberIsInPipMode() + if (!isInPipMode || player == null) { + // Broadcast receiver is only used if app is in PiP mode and player is non null + return + } + val context = LocalContext.current + + DisposableEffect(player) { + val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) { + return + } + + when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) { + EXTRA_CONTROL_PAUSE -> player.pause() + EXTRA_CONTROL_PLAY -> player.play() + } + } + } + ContextCompat.registerReceiver( + context, + broadcastReceiver, + IntentFilter(ACTION_BROADCAST_CONTROL), + ContextCompat.RECEIVER_NOT_EXPORTED + ) + onDispose { + context.unregisterReceiver(broadcastReceiver) + } + } +} +// [END android_compose_pip_broadcast_receiver] + +@RequiresApi(Build.VERSION_CODES.O) +fun listOfRemoteActions(): List { + return listOf() +} + +@Composable +fun PiPBuilderAddRemoteActions( + player: Player?, + shouldEnterPipMode: Boolean, + modifier: Modifier = Modifier, +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // [START android_compose_pip_add_remote_actions] + val context = LocalContext.current + + val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> + val builder = PictureInPictureParams.Builder() + builder.setActions( + listOfRemoteActions() + ) + + if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { + val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() + builder.setSourceRectHint(sourceRect) + builder.setAspectRatio( + Rational(player.videoSize.width, player.videoSize.height) + ) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(shouldEnterPipMode) + } + context.findActivity().setPictureInPictureParams(builder.build()) + } + VideoPlayer(modifier = pipModifier) + // [END android_compose_pip_add_remote_actions] + } else { + Log.i(PIP_TAG, "API does not support PiP") + } +} + +@Composable +fun PipListenerPreAPI12(shouldEnterPipMode: Boolean) { + // [START android_compose_pip_pre12_listener] + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + Build.VERSION.SDK_INT < Build.VERSION_CODES.S + ) { + val context = LocalContext.current + DisposableEffect(context) { + val onUserLeaveBehavior = Runnable { + context.findActivity() + .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) + } + context.findActivity().addOnUserLeaveHintListener( + onUserLeaveBehavior + ) + onDispose { + context.findActivity().removeOnUserLeaveHintListener( + onUserLeaveBehavior + ) + } + } + } else { + Log.i("PiP info", "API does not support PiP") + } + // [END android_compose_pip_pre12_listener] +} + +@Composable +fun EnterPiPPre12(shouldEnterPipMode: Boolean) { + // [START android_compose_pip_pre12_should_enter_pip] + val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + Build.VERSION.SDK_INT < Build.VERSION_CODES.S + ) { + val context = LocalContext.current + DisposableEffect(context) { + val onUserLeaveBehavior = Runnable { + if (currentShouldEnterPipMode) { + context.findActivity() + .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) + } + } + context.findActivity().addOnUserLeaveHintListener( + onUserLeaveBehavior + ) + onDispose { + context.findActivity().removeOnUserLeaveHintListener( + onUserLeaveBehavior + ) + } + } + } else { + Log.i("PiP info", "API does not support PiP") + } + // [END android_compose_pip_pre12_should_enter_pip] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt new file mode 100644 index 000000000..f9999f67c --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.predictiveback + +import android.os.SystemClock +import androidx.activity.BackEventCompat +import androidx.activity.compose.PredictiveBackHandler +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.flow.Flow +import kotlinx.serialization.Serializable + +@Serializable data object Home +@Serializable data object Settings + +@Composable +private fun PredictiveBackOverrideExit( + modifier: Modifier, +) { + val navController = rememberNavController() + + // [START android_compose_predictiveback_navhost] + NavHost( + navController = navController, + startDestination = Home, + popExitTransition = { + scaleOut( + targetScale = 0.9f, + transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.5f) + ) + }, + popEnterTransition = { + EnterTransition.None + }, + modifier = modifier, + ) + // [END android_compose_predictiveback_navhost] + { + composable { + HomeScreen( + modifier = modifier, + navController = navController, + ) + } + composable { + SettingsScreen( + modifier = modifier, + navController = navController, + ) + } + } +} + +@Composable +private fun HomeScreen( + modifier: Modifier = Modifier, + navController: NavHostController +) { +} + +@Composable +private fun SettingsScreen( + modifier: Modifier = Modifier, + navController: NavHostController +) { +} + +@Composable +private fun PredictiveBackHandlerBasicExample() { + + var boxScale by remember { mutableFloatStateOf(1F) } + + Box( + modifier = Modifier + .graphicsLayer { + scaleX = boxScale + scaleY = scaleX + } + .background(Color.Blue) + ) + + // [START android_compose_predictivebackhandler_basic] + PredictiveBackHandler(true) { progress: Flow -> + // code for gesture back started + try { + progress.collect { backEvent -> + // code for progress + boxScale = 1F - (1F * backEvent.progress) + } + // code for completion + boxScale = 0F + } catch (e: CancellationException) { + // code for cancellation + boxScale = 1F + throw e + } + } + // [END android_compose_predictivebackhandler_basic] +} + +@Composable +private fun PredictiveBackHandlerManualProgress() { + + Surface( + modifier = Modifier.fillMaxSize() + ) { + var drawerState by remember { + mutableStateOf(DrawerState.Closed) + } + + val translationX = remember { + Animatable(0f) + } + + val drawerWidth = with(LocalDensity.current) { + DrawerWidth.toPx() + } + translationX.updateBounds(0f, drawerWidth) + + val coroutineScope = rememberCoroutineScope() + + suspend fun closeDrawer(velocity: Float = 0f) { + translationX.animateTo(targetValue = 0f, initialVelocity = velocity) + drawerState = DrawerState.Closed + } + suspend fun openDrawer(velocity: Float = 0f) { + translationX.animateTo(targetValue = drawerWidth, initialVelocity = velocity) + drawerState = DrawerState.Open + } + + val velocityTracker = remember { + VelocityTracker() + } + + // [START android_compose_predictivebackhandler_manualprogress] + PredictiveBackHandler(drawerState == DrawerState.Open) { progress -> + try { + progress.collect { backEvent -> + val targetSize = (drawerWidth - (drawerWidth * backEvent.progress)) + translationX.snapTo(targetSize) + velocityTracker.addPosition( + SystemClock.uptimeMillis(), + Offset(backEvent.touchX, backEvent.touchY) + ) + } + closeDrawer(velocityTracker.calculateVelocity().x) + } catch (e: CancellationException) { + openDrawer(velocityTracker.calculateVelocity().x) + throw e + } finally { + velocityTracker.resetTracking() + } + } + // [END android_compose_predictivebackhandler_manualprogress] + } +} + +private enum class DrawerState { + Open, + Closed +} + +private val DrawerWidth = 300.dp diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt new file mode 100644 index 000000000..f964e352d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/resources/ResourcesSnippets.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.resources + +import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Menu +import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import com.example.compose.snippets.R + +@Composable +fun Strings() { + // [START android_compose_resources_strings] + // In the res/values/strings.xml file + // Jetpack Compose + + // In your Compose code + Text( + text = stringResource(R.string.compose) + ) + // [END android_compose_resources_strings] + + // [START android_compose_resources_strings_formatting] + // In the res/values/strings.xml file + // Happy %1$s %2$d + + // In your Compose code + Text( + text = stringResource(R.string.congratulate, "New Year", 2021) + ) + // [END android_compose_resources_strings_formatting] + + val quantity = 1 + // [START android_compose_resources_strings_plural] + // In the res/strings.xml file + // + // %1$d minute + // %1$d minutes + // + + // In your Compose code + Text( + text = pluralStringResource( + R.plurals.runtime_format, + quantity, + quantity + ) + ) + // [END android_compose_resources_strings_plural] +} + +@Composable +fun Dimensions() { + // [START android_compose_resources_dimensions] + // In the res/values/dimens.xml file + // 8dp + + // In your Compose code + val smallPadding = dimensionResource(R.dimen.padding_small) + Text( + text = "...", + modifier = Modifier.padding(smallPadding) + ) + // [END android_compose_resources_dimensions] +} + +@Composable +fun Colors() { + // [START android_compose_resources_colors] + // In the res/colors.xml file + // #FFBB86FC + + // In your Compose code + HorizontalDivider(color = colorResource(R.color.purple_200)) + // [END android_compose_resources_colors] +} + +@Composable +fun VectorAssets() { + // [START android_compose_resources_vector_assets] + // Files in res/drawable folders. For example: + // - res/drawable-nodpi/ic_logo.xml + // - res/drawable-xxhdpi/ic_logo.png + + // In your Compose code + Icon( + painter = painterResource(id = R.drawable.ic_logo), + contentDescription = null // decorative element + ) + // [END android_compose_resources_vector_assets] +} + +@OptIn(ExperimentalAnimationGraphicsApi::class) +@Composable +fun AnimatedVectorDrawables() { + // [START android_compose_resources_avd] + // Files in res/drawable folders. For example: + // - res/drawable/ic_hourglass_animated.xml + + // In your Compose code + val image = + AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated) + val atEnd by remember { mutableStateOf(false) } + Icon( + painter = rememberAnimatedVectorPainter(image, atEnd), + contentDescription = null // decorative element + ) + // [END android_compose_resources_avd] +} + +@Composable +fun Icons() { + // [START android_compose_resources_icons] + Icon(Icons.Rounded.Menu, contentDescription = "Localized description") + // [END android_compose_resources_icons] +} + +// [START android_compose_resources_fonts] +// Define and load the fonts of the app +private val light = Font(R.font.raleway_light, FontWeight.W300) +private val regular = Font(R.font.raleway_regular, FontWeight.W400) +private val medium = Font(R.font.raleway_medium, FontWeight.W500) +private val semibold = Font(R.font.raleway_semibold, FontWeight.W600) + +// Create a font family to use in TextStyles +private val craneFontFamily = FontFamily(light, regular, medium, semibold) + +// Use the font family to define a custom typography +val craneTypography = Typography( + titleLarge = TextStyle( + fontFamily = craneFontFamily + ) /* ... */ +) + +// Pass the typography to a MaterialTheme that will create a theme using +// that typography in the part of the UI hierarchy where this theme is used +@Composable +fun CraneTheme(content: @Composable () -> Unit) { + MaterialTheme(typography = craneTypography) { + content() + } +} +// [END android_compose_resources_fonts] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt new file mode 100644 index 000000000..dde0b242e --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/semantics/SemanticsSnippets.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.semantics + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview + +@Preview +@Composable +private fun Merging() { + // [START android_compose_semantics_merging] + Button(onClick = { /*TODO*/ }) { + Icon( + imageVector = Icons.Filled.Favorite, + contentDescription = null + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text("Like") + } + // [END android_compose_semantics_merging] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt new file mode 100644 index 000000000..cba065a33 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/sideeffects/SideEffectsSnippets.kt @@ -0,0 +1,265 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.sideeffects + +import android.media.Image +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.example.compose.snippets.interop.FirebaseAnalytics +import com.example.compose.snippets.interop.User +import com.example.compose.snippets.kotlin.Message +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +@Composable +fun MyScreen() { +// [START android_compose_side_effects_launchedeffect] + // Allow the pulse rate to be configured, so it can be sped up if the user is running + // out of time + var pulseRateMs by remember { mutableLongStateOf(3000L) } + val alpha = remember { Animatable(1f) } + LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes + while (isActive) { + delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user + alpha.animateTo(0f) + alpha.animateTo(1f) + } + } +// [END android_compose_side_effects_launchedeffect] +} + +// [START_EXCLUDE silent] +class Movie { + val url = "" + val id = "" +} +// [END_EXCLUDE] +// [START android_compose_side_effects_remembercoroutinescope] +@Composable +fun MoviesScreen(snackbarHostState: SnackbarHostState) { + + // Creates a CoroutineScope bound to the MoviesScreen's lifecycle + val scope = rememberCoroutineScope() + + Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + } + ) { contentPadding -> + Column(Modifier.padding(contentPadding)) { + Button( + onClick = { + // Create a new coroutine in the event handler to show a snackbar + scope.launch { + snackbarHostState.showSnackbar("Something happened!") + } + } + ) { + Text("Press me") + } + } + } +} +// [END android_compose_side_effects_remembercoroutinescope] + +// [START android_compose_side_effects_rememberupdatedstate] +@Composable +fun LandingScreen(onTimeout: () -> Unit) { + + // This will always refer to the latest onTimeout function that + // LandingScreen was recomposed with + val currentOnTimeout by rememberUpdatedState(onTimeout) + + // Create an effect that matches the lifecycle of LandingScreen. + // If LandingScreen recomposes, the delay shouldn't start again. + LaunchedEffect(true) { + delay(SplashWaitTimeMillis) + currentOnTimeout() + } + + /* Landing screen content */ +} +// [START_EXCLUDE silent] +private const val SplashWaitTimeMillis = 1000L +// [END_EXCLUDE] +// [END android_compose_side_effects_rememberupdatedstate] + +// [START android_compose_side_effects_disposableeffect] +@Composable +fun HomeScreen( + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + onStart: () -> Unit, // Send the 'started' analytics event + onStop: () -> Unit // Send the 'stopped' analytics event +) { + // Safely update the current lambdas when a new one is provided + val currentOnStart by rememberUpdatedState(onStart) + val currentOnStop by rememberUpdatedState(onStop) + + // If `lifecycleOwner` changes, dispose and reset the effect + DisposableEffect(lifecycleOwner) { + // Create an observer that triggers our remembered callbacks + // for sending analytics events + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + currentOnStart() + } else if (event == Lifecycle.Event.ON_STOP) { + currentOnStop() + } + } + + // Add the observer to the lifecycle + lifecycleOwner.lifecycle.addObserver(observer) + + // When the effect leaves the Composition, remove the observer + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + /* Home screen content */ +} +// [END android_compose_side_effects_disposableeffect] + +// [START android_compose_side_effects_sideeffect] +@Composable +fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { + val analytics: FirebaseAnalytics = remember { + FirebaseAnalytics() + } + + // On every successful composition, update FirebaseAnalytics with + // the userType from the current User, ensuring that future analytics + // events have this metadata attached + SideEffect { + analytics.setUserProperty("userType", user.userType) + } + return analytics +} +// [END android_compose_side_effects_sideeffect] + +// b/368420773 +@Suppress("ProduceStateDoesNotAssignValue") +// [START android_compose_side_effects_producestate] +@Composable +fun loadNetworkImage( + url: String, + imageRepository: ImageRepository = ImageRepository() +): State> { + // Creates a State with Result.Loading as initial value + // If either `url` or `imageRepository` changes, the running producer + // will cancel and will be re-launched with the new inputs. + return produceState>(initialValue = Result.Loading, url, imageRepository) { + // In a coroutine, can make suspend calls + val image = imageRepository.load(url) + + // Update State with either an Error or Success result. + // This will trigger a recomposition where this State is read + value = if (image == null) { + Result.Error + } else { + Result.Success(image) + } + } +} +// [START_EXCLUDE silent] +class ImageRepository { + fun load(url: String): Image? { return null } +} +sealed class Result { + object Loading : Result() + object Error : Result() + class Success(t: T?) : Result() +} +// [END_EXCLUDE] +// [END android_compose_side_effects_producestate] + +// [START android_compose_side_effects_derivedstateof] +@Composable +// When the messages parameter changes, the MessageList +// composable recomposes. derivedStateOf does not +// affect this recomposition. +fun MessageList(messages: List) { + Box { + val listState = rememberLazyListState() + + LazyColumn(state = listState) { + // ... + } + + // Show the button if the first visible item is past + // the first item. We use a remembered derived state to + // minimize unnecessary compositions + val showButton by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 0 + } + } + + AnimatedVisibility(visible = showButton) { + ScrollToTopButton() + } + } +} +// [END android_compose_side_effects_derivedstateof] + +@Composable +fun ScrollToTopButton() { + // Button to scroll to the top of list. +} + +@Suppress("CanBeVal") +@Composable +fun DerivedStateOfWrongUsage() { + // [START android_compose_side_effects_derivedstateof_wrong] + // DO NOT USE. Incorrect usage of derivedStateOf. + var firstName by remember { mutableStateOf("") } + var lastName by remember { mutableStateOf("") } + + val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!! + val fullNameCorrect = "$firstName $lastName" // This is correct + // [END android_compose_side_effects_derivedstateof_wrong] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/state/CompositionLocalSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/state/CompositionLocalSnippets.kt new file mode 100644 index 000000000..a2c2aa6dc --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/state/CompositionLocalSnippets.kt @@ -0,0 +1,285 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.state + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.compose.snippets.R + +object CompositionLocalSnippets1 { + private val colors = colors() + + // [START android_compose_state_compositionlocal1] + @Composable + fun MyApp() { + // Theme information tends to be defined near the root of the application + val colors = colors() + } + + // Some composable deep in the hierarchy + @Composable + fun SomeTextLabel(labelText: String) { + Text( + text = labelText, + color = colors.onPrimary // ← need to access colors here + ) + } + // [END android_compose_state_compositionlocal1] +} + +object CompositionLocalSnippets2 { + // [START android_compose_state_compositionlocal2] + @Composable + fun MyApp() { + // Provides a Theme whose values are propagated down its `content` + MaterialTheme { + // New values for colorScheme, typography, and shapes are available + // in MaterialTheme's content lambda. + + // ... content here ... + } + } + + // Some composable deep in the hierarchy of MaterialTheme + @Composable + fun SomeTextLabel(labelText: String) { + Text( + text = labelText, + // `primary` is obtained from MaterialTheme's + // LocalColors CompositionLocal + color = MaterialTheme.colorScheme.primary + ) + } + // [END android_compose_state_compositionlocal2] +} + +object CompositionLocalSnippets3 { + // [START android_compose_state_compositionlocal3] + @Composable + fun CompositionLocalExample() { + MaterialTheme { + // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default + // This is to automatically make text and other content contrast to the background + // correctly. + Surface { + Column { + Text("Uses Surface's provided content color") + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { + Text("Primary color provided by LocalContentColor") + Text("This Text also uses primary as textColor") + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { + DescendantExample() + } + } + } + } + } + } + + @Composable + fun DescendantExample() { + // CompositionLocalProviders also work across composable functions + Text("This Text uses the error color now") + } + // [END android_compose_state_compositionlocal3] +} + +object CompositionLocalSnippets4 { + // [START android_compose_state_compositionlocal4] + @Composable + fun FruitText(fruitSize: Int) { + // Get `resources` from the current value of LocalContext + val resources = LocalContext.current.resources + val fruitText = remember(resources, fruitSize) { + resources.getQuantityString(R.plurals.fruit_title, fruitSize) + } + Text(text = fruitText) + } + // [END android_compose_state_compositionlocal4] +} + +object CompositionLocalSnippets5_6_7 { + // [START android_compose_state_compositionlocal5] + // LocalElevations.kt file + + data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp) + + // Define a CompositionLocal global object with a default + // This instance can be accessed by all composables in the app + val LocalElevations = compositionLocalOf { Elevations() } + // [END android_compose_state_compositionlocal5] + + // [START android_compose_state_compositionlocal6] + // MyActivity.kt file + + class MyActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + // Calculate elevations based on the system theme + val elevations = if (isSystemInDarkTheme()) { + Elevations(card = 1.dp, default = 1.dp) + } else { + Elevations(card = 0.dp, default = 0.dp) + } + + // Bind elevation as the value for LocalElevations + CompositionLocalProvider(LocalElevations provides elevations) { + // ... Content goes here ... + // This part of Composition will see the `elevations` instance + // when accessing LocalElevations.current + } + } + } + } + // [END android_compose_state_compositionlocal6] + + // [START android_compose_state_compositionlocal7] + @Composable + fun SomeComposable() { + // Access the globally defined LocalElevations variable to get the + // current Elevations in this part of the Composition + MyCard(elevation = LocalElevations.current.card) { + // Content + } + } + // [END android_compose_state_compositionlocal7] + + @Composable + fun MyCard(elevation: Dp, content: @Composable () -> Unit) { + } +} + +object CompositionLocalSnippets8 { + // [START android_compose_state_compositionlocal8] + @Composable + fun MyComposable(myViewModel: MyViewModel = viewModel()) { + // ... + MyDescendant(myViewModel.data) + } + + // Don't pass the whole object! Just what the descendant needs. + // Also, don't pass the ViewModel as an implicit dependency using + // a CompositionLocal. + @Composable + fun MyDescendant(myViewModel: MyViewModel) { /* ... */ } + + // Pass only what the descendant needs + @Composable + fun MyDescendant(data: DataToDisplay) { + // Display data + } + // [END android_compose_state_compositionlocal8] +} + +object CompositionLocalSnippets9 { + // [START android_compose_state_compositionlocal9] + @Composable + fun MyComposable(myViewModel: MyViewModel = viewModel()) { + // ... + MyDescendant(myViewModel) + } + + @Composable + fun MyDescendant(myViewModel: MyViewModel) { + Button(onClick = { myViewModel.loadData() }) { + Text("Load data") + } + } + // [END android_compose_state_compositionlocal9] +} + +object CompositionLocalSnippets10 { + // [START android_compose_state_compositionlocal10] + @Composable + fun MyComposable(myViewModel: MyViewModel = viewModel()) { + // ... + ReusableLoadDataButton( + onLoadClick = { + myViewModel.loadData() + } + ) + } + + @Composable + fun ReusableLoadDataButton(onLoadClick: () -> Unit) { + Button(onClick = onLoadClick) { + Text("Load data") + } + } + // [END android_compose_state_compositionlocal10] +} + +object CompositionLocalSnippets11 { + // [START android_compose_state_compositionlocal11] + @Composable + fun MyComposable(myViewModel: MyViewModel = viewModel()) { + // ... + ReusablePartOfTheScreen( + content = { + Button( + onClick = { + myViewModel.loadData() + } + ) { + Text("Confirm") + } + } + ) + } + + @Composable + fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { + Column { + // ... + content() + } + } + // [END android_compose_state_compositionlocal11] +} + +/*** + * Fakes + */ +fun colors(): ColorScheme = lightColorScheme() + +data class DataToDisplay(val title: String) +class MyViewModel : ViewModel() { + val data = DataToDisplay("") + + fun loadData() {} +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/state/SavingUIStateSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/state/SavingUIStateSnippets.kt new file mode 100644 index 000000000..cc35c128d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/state/SavingUIStateSnippets.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "UNUSED_PARAMETER") + +package com.example.compose.snippets.state + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onStart + +private object SavingUIStateSnippets1 { + // [START android_compose_state_saving_rememberSaveable] + @Composable + fun ChatBubble( + message: Message + ) { + var showDetails by rememberSaveable { mutableStateOf(false) } + + ClickableText( + text = AnnotatedString(message.content), + onClick = { showDetails = !showDetails } + ) + + if (showDetails) { + Text(message.timestamp) + } + } + // [END android_compose_state_saving_rememberSaveable] +} + +private object SavingUIStateSnippets2 { + // [START android_compose_state_with_saver] + @Composable + fun rememberLazyListState( + initialFirstVisibleItemIndex: Int = 0, + initialFirstVisibleItemScrollOffset: Int = 0 + ): LazyListState { + return rememberSaveable(saver = LazyListState.Saver) { + LazyListState( + initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset + ) + } + } + // [END android_compose_state_with_saver] +} + +private object SavingUIStateSnippets3 { + @OptIn(SavedStateHandleSaveableApi::class) + // [START android_compose_state_apis_saveable] + class ConversationViewModel( + savedStateHandle: SavedStateHandle + ) : ViewModel() { + + var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + private set + + fun update(newMessage: TextFieldValue) { + message = newMessage + } + + /*...*/ + } + + val viewModel = ConversationViewModel(SavedStateHandle()) + + @Composable + fun UserInput(/*...*/) { + TextField( + value = viewModel.message, + onValueChange = { viewModel.update(it) } + ) + } + // [END android_compose_state_apis_saveable] +} + +private object SavingUIStateSnippets4 { + class Channel + class ChannelsRepository { + fun getAll() = MutableSharedFlow>() + } + fun filter(channels: List, type: ChannelsFilterType) = listOf() + + // [START android_compose_state_apis_savedStateHandle] + private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey" + + class ChannelViewModel( + channelsRepository: ChannelsRepository, + private val savedStateHandle: SavedStateHandle + ) : ViewModel() { + + private val savedFilterType: StateFlow = savedStateHandle.getStateFlow( + key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS + ) + + private val filteredChannels: Flow> = + combine(channelsRepository.getAll(), savedFilterType) { channels, type -> + filter(channels, type) + }.onStart { emit(emptyList()) } + + fun setFiltering(requestType: ChannelsFilterType) { + savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType + } + + /*...*/ + } + + enum class ChannelsFilterType { + ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS + } + // [END android_compose_state_apis_savedStateHandle] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/state/StateHoistingSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/state/StateHoistingSnippets.kt new file mode 100644 index 000000000..2e607fde9 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/state/StateHoistingSnippets.kt @@ -0,0 +1,296 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "UNUSED_PARAMETER") + +package com.example.compose.snippets.state + +import android.R.id.message +import androidx.compose.foundation.clickable +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withLink +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private object StateHoistingSnippets1 { + // [START android_compose_state_hoisting_no_hoisting] + @Composable + fun ChatBubble( + message: Message + ) { + var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state + + Text( + text = AnnotatedString(message.content), + modifier = Modifier.clickable { + showDetails = !showDetails // Apply UI logic + } + ) + + if (showDetails) { + Text(message.timestamp) + } + } + // [END android_compose_state_hoisting_no_hoisting] +} + +private object StateHoistingSnippets2 { + var messages = listOf() + @Composable + fun UserInput(onMessageSent: () -> Unit) { + } + @Composable + fun JumpToBottom(onClicked: () -> Unit) { + } + + // [START android_compose_state_hoisting_composables] + @Composable + private fun ConversationScreen(/*...*/) { + val scope = rememberCoroutineScope() + + val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen + + MessagesList(messages, lazyListState) // Reuse same state in MessageList + + UserInput( + onMessageSent = { // Apply UI logic to lazyListState + scope.launch { + lazyListState.scrollToItem(0) + } + }, + ) + } + + @Composable + private fun MessagesList( + messages: List, + lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value + ) { + + LazyColumn( + state = lazyListState // Pass hoisted state to LazyColumn + ) { + items(messages, key = { message -> message.id }) { item -> + Message(/*...*/) + } + } + + val scope = rememberCoroutineScope() + + JumpToBottom(onClicked = { + scope.launch { + lazyListState.scrollToItem(0) // UI logic being applied to lazyListState + } + }) + } + // [END android_compose_state_hoisting_composables] +} + +private object StateHoistingSnippets3 { + // [START android_compose_state_hoisting_plain_class] + // LazyListState.kt + + @Stable + class LazyListState constructor( + firstVisibleItemIndex: Int = 0, + firstVisibleItemScrollOffset: Int = 0 + ) : ScrollableState { + /** + * The holder class for the current scroll position. + */ + private val scrollPosition = LazyListScrollPosition( + firstVisibleItemIndex, firstVisibleItemScrollOffset + ) + + suspend fun scrollToItem(/*...*/) { /*...*/ } + + override suspend fun scroll() { /*...*/ } + + suspend fun animateScrollToItem() { /*...*/ } + } + // [END android_compose_state_hoisting_plain_class] +} + +private object StateHoistingSnippets4 { + @Composable + private fun MessagesList(messages: List, onSendMessage: (Message) -> Unit) { + } + + // [START android_compose_state_hoisting_vm] + class ConversationViewModel( + channelId: String, + messagesRepository: MessagesRepository + ) : ViewModel() { + + val messages = messagesRepository + .getLatestMessages(channelId) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList() + ) + + // Business logic + fun sendMessage(message: Message) { /* ... */ } + } + // [END android_compose_state_hoisting_vm] + + // [START android_compose_state_hoisting_vm_usage] + @Composable + private fun ConversationScreen( + conversationViewModel: ConversationViewModel = viewModel() + ) { + + val messages by conversationViewModel.messages.collectAsStateWithLifecycle() + + ConversationScreen( + messages = messages, + onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) } + ) + } + + @Composable + private fun ConversationScreen( + messages: List, + onSendMessage: (Message) -> Unit + ) { + + MessagesList(messages, onSendMessage) + /* ... */ + } + // [END android_compose_state_hoisting_vm_usage] +} + +@OptIn(ExperimentalCoroutinesApi::class) +private object StateHoistingSnippets5 { + class Suggestion + class Repository { + fun getSuggestions(s: String) = listOf() + } + val repository = Repository() + private fun getHandle(s: String) = "" + private fun hasSocialHandleHint(s: String) = true + + // [START android_compose_state_hoisting_vm_ui_element_state] + class ConversationViewModel(/*...*/) : ViewModel() { + + // Hoisted state + var inputMessage by mutableStateOf("") + private set + + val suggestions: StateFlow> = + snapshotFlow { inputMessage } + .filter { hasSocialHandleHint(it) } + .mapLatest { getHandle(it) } + .mapLatest { repository.getSuggestions(it) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList() + ) + + fun updateInput(newInput: String) { + inputMessage = newInput + } + } + // [END android_compose_state_hoisting_vm_ui_element_state] +} + +private object StateHoistingSnippets6 { + @Composable + private fun ConversationScreen(onCloseDrawer: () -> Unit) {} + enum class DrawerContent { + Empty, Content + } + val content = DrawerContent.Content + + // [START android_compose_state_hoisting_vm_ui_element_state_caveat] + class ConversationViewModel(/*...*/) : ViewModel() { + + val drawerState = DrawerState(initialValue = DrawerValue.Closed) + + private val _drawerContent = MutableStateFlow(DrawerContent.Empty) + val drawerContent: StateFlow = _drawerContent.asStateFlow() + + fun closeDrawer(uiScope: CoroutineScope) { + viewModelScope.launch { + withContext(uiScope.coroutineContext) { // Use instead of the default context + drawerState.close() + } + // Fetch drawer content and update state + _drawerContent.update { content } + } + } + } + + // in Compose + @Composable + private fun ConversationScreen( + conversationViewModel: ConversationViewModel = viewModel() + ) { + val scope = rememberCoroutineScope() + + ConversationScreen(onCloseDrawer = { conversationViewModel.closeDrawer(uiScope = scope) }) + } + // [END android_compose_state_hoisting_vm_ui_element_state_caveat] +} + +data class Message(var id: String = "", var content: String = "", var timestamp: String = "") + +class LazyListScrollPosition(firstVisibleItemIndex: Int, firstVisibleItemScrollOffset: Int) + +interface ScrollableState { + suspend fun scroll() +} + +abstract class MessagesRepository() { + abstract fun getLatestMessages(channelId: String): StateFlow> +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/state/StateOverviewSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/state/StateOverviewSnippets.kt new file mode 100644 index 000000000..5ac1e71b4 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/state/StateOverviewSnippets.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.state + +import android.content.res.Resources +import android.graphics.BitmapShader +import android.graphics.Shader +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.mapSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowSizeClass + +// [START android_compose_state_overview] +@Composable +private fun HelloContent() { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Hello!", + modifier = Modifier.padding(bottom = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + OutlinedTextField( + value = "", + onValueChange = { }, + label = { Text("Name") } + ) + } +} +// [END android_compose_state_overview] + +private object StateSnippet1 { + // [START android_compose_state_remember] + @Composable + fun HelloContent() { + Column(modifier = Modifier.padding(16.dp)) { + var name by remember { mutableStateOf("") } + if (name.isNotEmpty()) { + Text( + text = "Hello, $name!", + modifier = Modifier.padding(bottom = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + } + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name") } + ) + } + } + // [END android_compose_state_remember] +} + +private object StateSnippet2 { + // [START android_compose_state_hoisting] + @Composable + fun HelloScreen() { + var name by rememberSaveable { mutableStateOf("") } + + HelloContent(name = name, onNameChange = { name = it }) + } + + @Composable + fun HelloContent(name: String, onNameChange: (String) -> Unit) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Hello, $name", + modifier = Modifier.padding(bottom = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) + } + } + // [END android_compose_state_hoisting] +} + +private object StateSnippet3 { + // [START android_compose_state_restoring_parcelize] + @Parcelize + data class City(val name: String, val country: String) : Parcelable + + @Composable + fun CityScreen() { + var selectedCity = rememberSaveable { + mutableStateOf(City("Madrid", "Spain")) + } + } + // [END android_compose_state_restoring_parcelize] +} + +private object StateSnippet4 { + // [START android_compose_state_restoring_mapSaver] + data class City(val name: String, val country: String) + + val CitySaver = run { + val nameKey = "Name" + val countryKey = "Country" + mapSaver( + save = { mapOf(nameKey to it.name, countryKey to it.country) }, + restore = { City(it[nameKey] as String, it[countryKey] as String) } + ) + } + + @Composable + fun CityScreen() { + var selectedCity = rememberSaveable(stateSaver = CitySaver) { + mutableStateOf(City("Madrid", "Spain")) + } + } + // [END android_compose_state_restoring_mapSaver] +} + +@Composable +private fun StateSnippets5() { + // [START android_compose_state_restoring_listSaver] + data class City(val name: String, val country: String) + + val CitySaver = listSaver( + save = { listOf(it.name, it.country) }, + restore = { City(it[0] as String, it[1] as String) } + ) + + @Composable + fun CityScreen() { + var selectedCity = rememberSaveable(stateSaver = CitySaver) { + mutableStateOf(City("Madrid", "Spain")) + } + } + // [END android_compose_state_restoring_listSaver] +} + +@Composable +private fun RememberKeysSnippet1() { + // [START android_compose_state_remember_definition] + var name by remember { mutableStateOf("") } + // [END android_compose_state_remember_definition] +} + +@Composable +private fun RememberKeysSnippet2(@DrawableRes avatarRes: Int) { + val res = LocalContext.current.resources + + // [START android_compose_state_remember_brush] + val brush = remember { + ShaderBrush( + BitmapShader( + ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), + Shader.TileMode.REPEAT, + Shader.TileMode.REPEAT + ) + ) + } + // [END android_compose_state_remember_brush] +} + +// [START android_compose_state_remember_keys_brush] +@Composable +private fun BackgroundBanner( + @DrawableRes avatarRes: Int, + modifier: Modifier = Modifier, + res: Resources = LocalContext.current.resources +) { + val brush = remember(key1 = avatarRes) { + ShaderBrush( + BitmapShader( + ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), + Shader.TileMode.REPEAT, + Shader.TileMode.REPEAT + ) + ) + } + + Box( + modifier = modifier.background(brush) + ) { + /* ... */ + } +} +// [END android_compose_state_remember_keys_brush] + +// [START android_compose_state_remember_keys_app_state] +@Composable +private fun rememberMyAppState( + windowSizeClass: WindowSizeClass +): MyAppState { + return remember(windowSizeClass) { + MyAppState(windowSizeClass) + } +} + +@Stable +class MyAppState( + private val windowSizeClass: WindowSizeClass +) { /* ... */ } +// [END android_compose_state_remember_keys_app_state] + +@Composable +private fun RememberKeysSnippet3() { + val typedQuery = "" + // [START android_compose_state_rememberSaveable_keys] + var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) + ) + } + // [END android_compose_state_rememberSaveable_keys] +} + +/** + * Add fake com.example.compose.snippets.state.Parcelize and com.example.compose.snippets.state.Parcelable to avoid adding dependency on + * kotlin-parcelize just for snippets + */ +private annotation class Parcelize +private interface Parcelable diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/stylus/StylusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/stylus/StylusSnippets.kt new file mode 100644 index 000000000..82c785984 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/stylus/StylusSnippets.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.stylus + +import android.view.MotionEvent +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.lifecycle.ViewModel + +class UserViewModel : ViewModel() { + fun processMotionEvent(e: MotionEvent): Boolean { + return true + } +} + +val viewModel = UserViewModel() + +// [START android_compose_stylus_motion_event_access] +@Composable +@OptIn(ExperimentalComposeUiApi::class) +fun DrawArea(modifier: Modifier = Modifier) { + Canvas( + modifier = modifier + .clipToBounds() + .pointerInteropFilter { + viewModel.processMotionEvent(it) + } + + ) { + // Drawing code here. + } +} +// [END android_compose_stylus_motion_event_access] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/system/CutoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/system/CutoutSnippets.kt new file mode 100644 index 000000000..d271d1eed --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/system/CutoutSnippets.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.system + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.safeContent +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat + +/* +* Copyright 2022 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +class CutoutExampleSnippet : ComponentActivity() { + // [START android_compose_cutouts_default] + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { + Box(Modifier.windowInsetsPadding(WindowInsets.safeContent)) { + // Any composable inside here will avoid drawing behind cutouts + } + } + } + // [END android_compose_cutouts_default] +} + +@Composable +fun CutoutManualHandling() { + // [START android_compose_cutouts_manual] + Canvas(modifier = Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.displayCutout)) { + drawRect(Color.Red, style = Stroke(2.dp.toPx())) + } + // [END android_compose_cutouts_manual] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/AutofillSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/AutofillSnippets.kt new file mode 100644 index 000000000..6e30ba93b --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/AutofillSnippets.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.text + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.LocalAutofillHighlightColor +import androidx.compose.foundation.text.input.rememberTextFieldState +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.TextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.ContentType +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalAutofillManager +import androidx.compose.ui.semantics.contentType +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.touchinput.Button + +@Composable +fun AddAutofill() { + // [START android_compose_autofill_1] + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.Username } + ) + // [END android_compose_autofill_1] +} + +@Composable +fun AddMultipleTypesOfAutofill() { + // [START android_compose_autofill_2] + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { + contentType = ContentType.Username + ContentType.EmailAddress + } + ) + // [END android_compose_autofill_2] +} + +@Composable +fun AutofillManager() { + // [START android_compose_autofill_3] + val autofillManager = LocalAutofillManager.current + // [END android_compose_autofill_3] +} + +@Composable +fun SaveDataWithAutofill() { + var textFieldValue = remember { + mutableStateOf(TextFieldValue("")) + } + // [START android_compose_autofill_4] + val autofillManager = LocalAutofillManager.current + + Column { + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.NewUsername } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.NewPassword } + ) + } + // [END android_compose_autofill_4] +} + +@Composable +fun SaveDataWithAutofillOnClick() { + // [START android_compose_autofill_5] + val autofillManager = LocalAutofillManager.current + + Column { + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.NewUsername }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.NewPassword }, + ) + + // Submit button + Button(onClick = { autofillManager?.commit() }) { Text("Reset credentials") } + } + // [END android_compose_autofill_5] +} + +@Composable +fun CustomAutofillHighlight(customHighlightColor: Color = Color.Red) { + // [START android_compose_autofill_6] + val customHighlightColor = Color.Red + + CompositionLocalProvider(LocalAutofillHighlightColor provides customHighlightColor) { + TextField( + state = rememberTextFieldState(), + modifier = Modifier.semantics { contentType = ContentType.Username } + ) + } + // [END android_compose_autofill_6] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiSnippets.kt new file mode 100644 index 000000000..7a32ed36c --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiSnippets.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "PreviewMustBeTopLevelFunction") + +package com.example.compose.snippets.text + +import android.os.Bundle +import android.widget.TextView +import androidx.activity.ComponentActivity +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatTextView +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.text.EmojiSupportMatch +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.viewinterop.AndroidViewBinding +import androidx.emoji2.widget.EmojiTextView +import com.example.compose.snippets.databinding.ExampleViewBinding +import com.example.compose.snippets.text.EmojiUtils.EMOJI_TEXT + +private object ModernEmojiComposeDisableSnippet { + @Composable + fun ModernEmojiComposeDisableSnippet() { + // [START android_compose_text_emoji] + Text( + text = "Hello $EMOJI_TEXT", + style = TextStyle( + platformStyle = PlatformTextStyle( + emojiSupportMatch = EmojiSupportMatch.None + )/* ... */ + ) + ) + // [END android_compose_text_emoji] + } +} + +private object EmojiSnippets2 { + // [START android_compose_text_emoji_compatibility_component] + class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val emojiTextView: EmojiTextView = findViewById(R.id.emoji_text_view) + emojiTextView.text = getString(R.string.emoji_text_view, EMOJI_TEXT) + + val composeView: ComposeView = findViewById(R.id.compose_view) + + composeView.apply { + setContent { + // compose code + } + } + } + } + // [END android_compose_text_emoji_compatibility_component] + + object R { + object id { + const val emoji_text_view = 1 + const val compose_view = 2 + } + + object layout { + const val activity_main = 0 + } + + object string { + const val emoji_text_view = 0 + } + } +} + +private object EmojiSnippets3 { + + // [START android_compose_text_emoji_compatibility_textview] + class MyActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val emojiTextView: TextView = findViewById(R.id.emoji_text_view) + emojiTextView.text = getString(R.string.emoji_text_view, EMOJI_TEXT) + + val composeView: ComposeView = findViewById(R.id.compose_view) + + composeView.apply { + setContent { + // compose code + } + } + } + } + // [END android_compose_text_emoji_compatibility_textview] + + object R { + object id { + const val emoji_text_view = 1 + const val compose_view = 2 + } + + object layout { + const val activity_main = 0 + } + + object string { + const val emoji_text_view = 0 + } + } +} + +private object EmojiSnippets4 { + // [START android_compose_text_emoji_compatibility_viewbinding] + class MyActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView( + ComposeView(this).apply { + setContent { + Column { + Text(EMOJI_TEXT) + + AndroidViewBinding(ExampleViewBinding::inflate) { + emojiTextView.text = EMOJI_TEXT + } + } + } + } + ) + } + } + // [END android_compose_text_emoji_compatibility_viewbinding] +} + +private object EmojiSnippets5 { + // [START android_compose_text_emoji_compatibility_compat] + class MyActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView( + ComposeView(this).apply { + setContent { + Column { + Text(EMOJI_TEXT) + + AndroidView( + factory = { context -> AppCompatTextView(context) }, + update = { it.text = EMOJI_TEXT } + ) + } + } + } + ) + } + } + // [END android_compose_text_emoji_compatibility_compat] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiUtils.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiUtils.kt new file mode 100644 index 000000000..cdebf6c90 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/EmojiUtils.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.text + +object EmojiUtils { + const val EMOJI_TEXT = "emoji text" +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/FilterText.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/FilterText.kt new file mode 100644 index 000000000..6967ea4f8 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/FilterText.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.text + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +// [START android_compose_text_filtertextviewmodel] +class FilterTextViewModel : ViewModel() { + private val items = listOf( + "Cupcake", + "Donut", + "Eclair", + "Froyo", + "Gingerbread", + "Honeycomb", + "Ice Cream Sandwich" + ) + + private val _filteredItems = MutableStateFlow(items) + var filteredItems: StateFlow> = _filteredItems + + fun filterText(input: String) { + // This filter returns the full items list when input is an empty string. + _filteredItems.value = items.filter { it.contains(input, ignoreCase = true) } + } +} +// [END android_compose_text_filtertextviewmodel] + +// [START android_compose_text_filtertextview] +@Composable +fun FilterTextView(modifier: Modifier = Modifier, viewModel: FilterTextViewModel = viewModel()) { + val filteredItems by viewModel.filteredItems.collectAsStateWithLifecycle() + var text by rememberSaveable { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(all = 10.dp) + ) { + OutlinedTextField( + value = text, + onValueChange = { + text = it + viewModel.filterText(text) + }, + label = { Text("Filter Text") }, + modifier = Modifier.fillMaxWidth() + ) + + LazyColumn { + items( + count = filteredItems.size, + key = { index -> filteredItems[index] } + ) { + ListItem( + headlineContent = { Text(filteredItems[it]) }, + modifier = Modifier + .fillParentMaxWidth() + .padding(10.dp) + ) + } + } + } +} +// [END android_compose_text_filtertextview] + +@Preview(showBackground = true) +@Composable +private fun FilterTextViewPreview() { + FilterTextView() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/HtmlStyling.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/HtmlStyling.kt new file mode 100644 index 000000000..607fdf462 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/HtmlStyling.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.text + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview + +// [START android_compose_text_annotatedhtmlstringwithlink] +@Composable +fun AnnotatedHtmlStringWithLink( + modifier: Modifier = Modifier, + htmlText: String = """ +

Jetpack Compose

+

+ Build better apps faster with Jetpack Compose +

+ """.trimIndent() +) { + Text( + AnnotatedString.fromHtml( + htmlText, + linkStyles = TextLinkStyles( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + fontStyle = FontStyle.Italic, + color = Color.Blue + ) + ) + ), + modifier + ) +} +// [END android_compose_text_annotatedhtmlstringwithlink] + +@Preview(showBackground = true) +@Composable +private fun AnnotatedHtmlStringWithLinkPreview() { + AnnotatedHtmlStringWithLink() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt new file mode 100644 index 000000000..e4e901bd8 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/StateBasedText.kt @@ -0,0 +1,296 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.OutputTransformation +import androidx.compose.foundation.text.input.TextFieldBuffer +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.foundation.text.input.insert +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.selectAll +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.foundation.text.input.then +import androidx.compose.material.OutlinedTextField +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.TextField +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.text.isDigitsOnly +import androidx.lifecycle.ViewModel + +@Preview +@Composable +fun StateBasedTextSnippets() { + Column() { + // [START android_compose_state_text_0] + TextField( + state = rememberTextFieldState(initialText = "Hello"), + label = { Text("Label") } + ) + // [END android_compose_state_text_0] + + // [START android_compose_state_text_1] + OutlinedTextField( + state = rememberTextFieldState(), + label = { Text("Label") } + ) + // [END android_compose_state_text_1] + } +} + +@Preview +@Composable +fun StyleTextField() { + // [START android_compose_state_text_2] + TextField( + state = rememberTextFieldState("Hello\nWorld\nInvisible"), + lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 2), + placeholder = { Text("") }, + textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold), + label = { Text("Enter text") }, + modifier = Modifier.padding(20.dp) + ) + // [END android_compose_state_text_2] +} + +@Composable +fun ConfigureLineLimits() { + // [START android_compose_state_text_3] + TextField( + state = rememberTextFieldState(), + lineLimits = TextFieldLineLimits.SingleLine + ) + // [END android_compose_state_text_3] +} + +@Preview +@Composable +fun Multiline() { + Spacer(modifier = Modifier.height(15.dp)) + // [START android_compose_state_text_4] + TextField( + state = rememberTextFieldState("Hello\nWorld\nHello\nWorld"), + lineLimits = TextFieldLineLimits.MultiLine(1, 4) + ) + // [END android_compose_state_text_4] +} + +@Composable +fun StyleWithBrush() { + // [START android_compose_state_text_5] + val brush = remember { + Brush.linearGradient( + colors = listOf(Color.Red, Color.Yellow, Color.Green, Color.Blue, Color.Magenta) + ) + } + TextField( + state = rememberTextFieldState(), textStyle = TextStyle(brush = brush) + ) + // [END android_compose_state_text_5] +} + +@Composable +fun StateHoisting() { + // [START android_compose_state_text_6] + val usernameState = rememberTextFieldState() + TextField( + state = usernameState, + lineLimits = TextFieldLineLimits.SingleLine, + placeholder = { Text("Enter Username") } + ) + // [END android_compose_state_text_6] +} + +@Composable +fun TextFieldInitialState() { + // [START android_compose_state_text_7] + TextField( + state = rememberTextFieldState(initialText = "Username"), + lineLimits = TextFieldLineLimits.SingleLine, + ) + // [END android_compose_state_text_7] +} + +@Preview(showBackground = true) +@Composable +fun TextFieldBuffer() { + // [START android_compose_state_text_8] + val phoneNumberState = rememberTextFieldState("1234567890") + + TextField( + state = phoneNumberState, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Phone + ), + inputTransformation = InputTransformation.maxLength(10).then { + if (!asCharSequence().isDigitsOnly()) { + revertAllChanges() + } + }, + outputTransformation = OutputTransformation { + if (length > 0) insert(0, "(") + if (length > 4) insert(4, ")") + if (length > 8) insert(8, "-") + } + ) + // [END android_compose_state_text_8] +} + +@Preview +@Composable +fun EditTextFieldState() { + val usernameState = rememberTextFieldState("I love Android") + editTFState(usernameState) +} + +fun editTFState(textFieldState: TextFieldState) { + // [START android_compose_state_text_9] + // Initial textFieldState text passed in is "I love Android" + // textFieldState.text : I love Android + // textFieldState.selection: TextRange(14, 14) + textFieldState.edit { insert(14, "!") } + // textFieldState.text : I love Android! + // textFieldState.selection: TextRange(15, 15) + textFieldState.edit { replace(7, 14, "Compose") } + // textFieldState.text : I love Compose! + // textFieldState.selection: TextRange(15, 15) + textFieldState.edit { append("!!!") } + // textFieldState.text : I love Compose!!!! + // textFieldState.selection: TextRange(18, 18) + textFieldState.edit { selectAll() } + // textFieldState.text : I love Compose!!!! + // textFieldState.selection: TextRange(0, 18) + // [END android_compose_state_text_9] + + // [START android_compose_state_text_10] + textFieldState.setTextAndPlaceCursorAtEnd("I really love Android") + // textFieldState.text : I really love Android + // textFieldState.selection : TextRange(21, 21) + // [END android_compose_state_text_10] + + // [START android_compose_state_text_11] + textFieldState.clearText() + // textFieldState.text : + // textFieldState.selection : TextRange(0, 0) + // [END android_compose_state_text_11] +} + +class TextFieldViewModel : ViewModel() { + val usernameState = TextFieldState() + fun validateUsername() { + } +} +val textFieldViewModel = TextFieldViewModel() + +@Composable +fun TextFieldKeyboardOptions() { + // [START android_compose_state_text_13] + TextField( + state = textFieldViewModel.usernameState, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + onKeyboardAction = { performDefaultAction -> + textFieldViewModel.validateUsername() + performDefaultAction() + } + ) + // [END android_compose_state_text_13] +} + +@Composable +fun TextFieldInputTransformation() { + // [START android_compose_state_text_14] + TextField( + state = rememberTextFieldState(), + lineLimits = TextFieldLineLimits.SingleLine, + inputTransformation = InputTransformation.maxLength(10) + ) + // [END android_compose_state_text_14] +} + +// [START android_compose_state_text_15] +class CustomInputTransformation : InputTransformation { + override fun TextFieldBuffer.transformInput() { + } +} +// [END android_compose_state_text_15] + +// [START android_compose_state_text_16] +class DigitOnlyInputTransformation : InputTransformation { + override fun TextFieldBuffer.transformInput() { + if (!asCharSequence().isDigitsOnly()) { + revertAllChanges() + } + } +} +// [END android_compose_state_text_16] + +@Composable +fun ChainInputTransformation() { + // [START android_compose_state_text_17] + TextField( + state = rememberTextFieldState(), + inputTransformation = InputTransformation.maxLength(6) + .then(CustomInputTransformation()), + ) + // [END android_compose_state_text_17] +} + +// [START android_compose_state_text_18] +class CustomOutputTransformation : OutputTransformation { + override fun TextFieldBuffer.transformOutput() { + } +} +// [END android_compose_state_text_18] + +// [START android_compose_state_text_19] +class PhoneNumberOutputTransformation : OutputTransformation { + override fun TextFieldBuffer.transformOutput() { + if (length > 0) insert(0, "(") + if (length > 4) insert(4, ")") + if (length > 8) insert(8, "-") + } +} +// [END android_compose_state_text_19] + +@Composable +fun TextFieldOutputTransformation() { + // [START android_compose_state_text_20] + TextField( + state = rememberTextFieldState(), + outputTransformation = PhoneNumberOutputTransformation() + ) + // [END android_compose_state_text_20] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextDownloadableFontsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextDownloadableFontsSnippets.kt new file mode 100644 index 000000000..4eaffb648 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextDownloadableFontsSnippets.kt @@ -0,0 +1,235 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "PreviewMustBeTopLevelFunction") + +package com.example.compose.snippets.text + +import android.util.Log +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.googlefonts.Font +import androidx.compose.ui.text.googlefonts.GoogleFont +import androidx.compose.ui.text.googlefonts.isAvailableOnDevice +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.text.DownloadableFontsDebugSnippet.handler +import com.example.compose.snippets.text.TextDownloadableFontsSnippet1.provider +import kotlinx.coroutines.CoroutineExceptionHandler + +/** + * This file lets DevRel track changes to snippets present in + * https://developer.android.com/jetpack/compose/text + * + * No action required if it's modified. + */ + +private object TextDownloadableFontsSnippet1 { + // [START android_compose_text_df_provider] + val provider = GoogleFont.Provider( + providerAuthority = "com.google.android.gms.fonts", + providerPackage = "com.google.android.gms", + certificates = R.array.com_google_android_gms_fonts_certs + ) + // [END android_compose_text_df_provider] +} + +private object TextDownloadableFontsSnippet2 { + // [START android_compose_text_df_fontFamily] + + // [START_EXCLUDE] + /** + // [END_EXCLUDE] + import androidx.compose.ui.text.googlefonts.GoogleFont + import androidx.compose.ui.text.font.FontFamily + import androidx.compose.ui.text.googlefonts.Font + // [START_EXCLUDE] + **/ + // [END_EXCLUDE] + + val fontName = GoogleFont("Lobster Two") + + val fontFamily = FontFamily( + Font(googleFont = fontName, fontProvider = provider) + ) + // [END android_compose_text_df_fontFamily] +} + +private object TextDownloadableFontsSnippet3 { + // [START android_compose_text_df_fontFamily_style] + + // [START_EXCLUDE] + /** + // [END_EXCLUDE] + import androidx.compose.ui.text.googlefonts.GoogleFont + import androidx.compose.ui.text.font.FontFamily + import androidx.compose.ui.text.googlefonts.Font + // [START_EXCLUDE] + **/ + // [END_EXCLUDE] + + val fontName = GoogleFont("Lobster Two") + + val fontFamily = FontFamily( + Font( + googleFont = fontName, + fontProvider = provider, + weight = FontWeight.Bold, + style = FontStyle.Italic + ) + ) + // [END android_compose_text_df_fontFamily_style] +} + +@Composable +fun DownloadableFontsText() { + // [START android_compose_text_df_fontFamily_usage] + Text( + fontFamily = fontFamily, text = "Hello World!" + ) + // [END android_compose_text_df_fontFamily_usage] +} + +private object TextDownloadableFontsSnippet4 { + // [START android_compose_text_typography_definition] + val MyTypography = Typography( + bodyMedium = TextStyle( + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 12.sp/*...*/ + ), + bodyLarge = TextStyle( + fontFamily = fontFamily, + fontWeight = FontWeight.Bold, + letterSpacing = 2.sp, + /*...*/ + ), + headlineMedium = TextStyle( + fontFamily = fontFamily, fontWeight = FontWeight.SemiBold/*...*/ + ), + /*...*/ + ) + // [END android_compose_text_typography_definition] + + @Composable + fun MyAppTheme(typography: Typography) { + } + + @Composable + fun MyApp() { + // [START android_compose_text_app_typography] + MyAppTheme( + typography = MyTypography + )/*...*/ + // [END android_compose_text_app_typography] + } +} + +private object FallbackFontsSnippet1 { + // [START android_compose_text_df_fallbacks] + + // [START_EXCLUDE] + /** + // [END_EXCLUDE] + import androidx.compose.ui.text.googlefonts.Font + // [START_EXCLUDE] + **/ + // [END_EXCLUDE] + + val fontName = GoogleFont("Lobster Two") + + val fontFamily = FontFamily( + Font(googleFont = fontName, fontProvider = provider), + Font(googleFont = fontName, fontProvider = provider, weight = FontWeight.Bold) + ) + // [END android_compose_text_df_fallbacks] +} + +private object FallbackFontsSnippet2 { + // [START android_compose_text_df_fallbacks_style] + + // [START_EXCLUDE] + /** + // [END_EXCLUDE] + import androidx.compose.ui.text.font.Font + import androidx.compose.ui.text.googlefonts.Font + // [START_EXCLUDE] + **/ + // [END_EXCLUDE] + + val fontName = GoogleFont("Lobster Two") + + val fontFamily = FontFamily( + Font(googleFont = fontName, fontProvider = provider), + Font(resId = R.font.my_font_regular), + Font(googleFont = fontName, fontProvider = provider, weight = FontWeight.Bold), + Font(resId = R.font.my_font_regular_bold, weight = FontWeight.Bold) + ) + // [END android_compose_text_df_fallbacks_style] +} + +private object DownloadableFontsDebugSnippet { + // [START android_compose_text_df_debug_apis_handler] + val handler = CoroutineExceptionHandler { _, throwable -> + // process the Throwable + Log.e(TAG, "There has been an issue: ", throwable) + } + // [END android_compose_text_df_debug_apis_handler] +} + +private object DownloadableFontsDebugSnippet2 { + @Composable + fun DownloadableFontsDebugSnippet() { + // [START android_compose_text_df_debug_apis_handler_setup] + CompositionLocalProvider( + LocalFontFamilyResolver provides createFontFamilyResolver(LocalContext.current, handler) + ) { + Column { + Text( + text = "Hello World!", style = MaterialTheme.typography.bodyMedium + ) + } + } + // [END android_compose_text_df_debug_apis_handler_setup] + } +} + +private object DownloadableFontsDebugSnippet3 { + @Composable + fun DownloadableFontsDebugSnippet() { + // [START android_compose_text_df_debug_apis] + val context = LocalContext.current + LaunchedEffect(Unit) { + if (provider.isAvailableOnDevice(context)) { + Log.d(TAG, "Success!") + } + } + // [END android_compose_text_df_debug_apis] + } +} + +private val fontFamily = FontFamily() +private const val TAG = "" diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextFakes.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextFakes.kt new file mode 100644 index 000000000..69e6daa0a --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextFakes.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.text + +/* + * Fakes needed for snippets to build: + */ +object R { + object string { + const val hello_world = 1 + } + + object font { + const val firasans_light = 1 + const val firasans_regular = 1 + const val firasans_italic = 1 + const val firasans_medium = 1 + const val firasans_bold = 1 + const val my_font_regular = 0 + const val my_font_regular_bold = 0 + } + + object array { + const val com_google_android_gms_fonts_certs = 1 + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextFieldMigrationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextFieldMigrationSnippets.kt new file mode 100644 index 000000000..a9e970266 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextFieldMigrationSnippets.kt @@ -0,0 +1,345 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.text + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.OutputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.delete +import androidx.compose.foundation.text.input.insert +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.SecureTextField +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.Text +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.substring +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.example.compose.snippets.touchinput.Button +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update + +// [START android_compose_text_textfield_migration_old_simple] +@Composable +fun OldSimpleTextField() { + var state by rememberSaveable { mutableStateOf("") } + TextField( + value = state, + onValueChange = { state = it }, + singleLine = true, + ) +} +// [END android_compose_text_textfield_migration_old_simple] + +// [START android_compose_text_textfield_migration_new_simple] +@Composable +fun NewSimpleTextField() { + TextField( + state = rememberTextFieldState(), + lineLimits = TextFieldLineLimits.SingleLine + ) +} +// [END android_compose_text_textfield_migration_new_simple] + +// [START android_compose_text_textfield_migration_old_filtering] +@Composable +fun OldNoLeadingZeroes() { + var input by rememberSaveable { mutableStateOf("") } + TextField( + value = input, + onValueChange = { newText -> + input = newText.trimStart { it == '0' } + } + ) +} +// [END android_compose_text_textfield_migration_old_filtering] + +// [START android_compose_text_textfield_migration_new_filtering] + +@Preview +@Composable +fun NewNoLeadingZeros() { + TextField( + state = rememberTextFieldState(), + inputTransformation = InputTransformation { + while (length > 0 && charAt(0) == '0') delete(0, 1) + } + ) +} +// [END android_compose_text_textfield_migration_new_filtering] + +// [START android_compose_text_textfield_migration_old_credit_card_formatter] +@Composable +fun OldTextFieldCreditCardFormatter() { + var state by remember { mutableStateOf("") } + TextField( + value = state, + onValueChange = { if (it.length <= 16) state = it }, + visualTransformation = VisualTransformation { text -> + // Making XXXX-XXXX-XXXX-XXXX string. + var out = "" + for (i in text.indices) { + out += text[i] + if (i % 4 == 3 && i != 15) out += "-" + } + + TransformedText( + text = AnnotatedString(out), + offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + if (offset <= 3) return offset + if (offset <= 7) return offset + 1 + if (offset <= 11) return offset + 2 + if (offset <= 16) return offset + 3 + return 19 + } + + override fun transformedToOriginal(offset: Int): Int { + if (offset <= 4) return offset + if (offset <= 9) return offset - 1 + if (offset <= 14) return offset - 2 + if (offset <= 19) return offset - 3 + return 16 + } + } + ) + } + ) +} +// [END android_compose_text_textfield_migration_old_credit_card_formatter] + +// [START android_compose_text_textfield_migration_new_credit_card_formatter] +@Composable +fun NewTextFieldCreditCardFormatter() { + val state = rememberTextFieldState() + TextField( + state = state, + inputTransformation = InputTransformation.maxLength(16), + outputTransformation = OutputTransformation { + if (length > 4) insert(4, "-") + if (length > 9) insert(9, "-") + if (length > 14) insert(14, "-") + }, + ) +} +// [END android_compose_text_textfield_migration_new_credit_card_formatter] + +private object StateUpdateSimpleSnippet { + object UserRepository { + suspend fun fetchUsername(): String = TODO() + } + // [START android_compose_text_textfield_migration_old_update_state_simple] + @Composable + fun OldTextFieldStateUpdate(userRepository: UserRepository) { + var username by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + username = userRepository.fetchUsername() + } + TextField( + value = username, + onValueChange = { username = it } + ) + } + // [END android_compose_text_textfield_migration_old_update_state_simple] + + // [START android_compose_text_textfield_migration_new_update_state_simple] + @Composable + fun NewTextFieldStateUpdate(userRepository: UserRepository) { + val usernameState = rememberTextFieldState() + LaunchedEffect(Unit) { + usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) + } + TextField(state = usernameState) + } + // [END android_compose_text_textfield_migration_new_update_state_simple] +} + +// [START android_compose_text_textfield_migration_old_state_update_complex] +@Composable +fun OldTextFieldAddMarkdownEmphasis() { + var markdownState by remember { mutableStateOf(TextFieldValue()) } + Button(onClick = { + // add ** decorations around the current selection, also preserve the selection + markdownState = with(markdownState) { + copy( + text = buildString { + append(text.take(selection.min)) + append("**") + append(text.substring(selection)) + append("**") + append(text.drop(selection.max)) + }, + selection = TextRange(selection.min + 2, selection.max + 2) + ) + } + }) { + Text("Bold") + } + TextField( + value = markdownState, + onValueChange = { markdownState = it }, + maxLines = 10 + ) +} +// [END android_compose_text_textfield_migration_old_state_update_complex] + +// [START android_compose_text_textfield_migration_new_state_update_complex] +@Composable +fun NewTextFieldAddMarkdownEmphasis() { + val markdownState = rememberTextFieldState() + LaunchedEffect(Unit) { + // add ** decorations around the current selection + markdownState.edit { + insert(originalSelection.max, "**") + insert(originalSelection.min, "**") + selection = TextRange(originalSelection.min + 2, originalSelection.max + 2) + } + } + TextField( + state = markdownState, + lineLimits = TextFieldLineLimits.MultiLine(1, 10) + ) +} +// [END android_compose_text_textfield_migration_new_state_update_complex] + +private object ViewModelMigrationOldSnippet { + // [START android_compose_text_textfield_migration_old_viewmodel] + class LoginViewModel : ViewModel() { + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow + get() = _uiState.asStateFlow() + + fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } + + fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } + } + + data class UiState( + val username: String = "", + val password: String = "" + ) + + @Composable + fun LoginForm( + loginViewModel: LoginViewModel, + modifier: Modifier = Modifier + ) { + val uiState by loginViewModel.uiState.collectAsStateWithLifecycle() + Column(modifier) { + TextField( + value = uiState.username, + onValueChange = { loginViewModel.updateUsername(it) } + ) + TextField( + value = uiState.password, + onValueChange = { loginViewModel.updatePassword(it) }, + visualTransformation = PasswordVisualTransformation() + ) + } + } + // [END android_compose_text_textfield_migration_old_viewmodel] +} + +private object ViewModelMigrationNewSimpleSnippet { + // [START android_compose_text_textfield_migration_new_viewmodel_simple] + class LoginViewModel : ViewModel() { + val usernameState = TextFieldState() + val passwordState = TextFieldState() + } + + @Composable + fun LoginForm( + loginViewModel: LoginViewModel, + modifier: Modifier = Modifier + ) { + Column(modifier) { + TextField(state = loginViewModel.usernameState,) + SecureTextField(state = loginViewModel.passwordState) + } + } + // [END android_compose_text_textfield_migration_new_viewmodel_simple] +} + +private object ViewModelMigrationNewConformingSnippet { + // [START android_compose_text_textfield_migration_new_viewmodel_conforming] + class LoginViewModel : ViewModel() { + private val _uiState = MutableStateFlow(UiState()) + val uiState: StateFlow + get() = _uiState.asStateFlow() + + fun updateUsername(username: String) = _uiState.update { it.copy(username = username) } + + fun updatePassword(password: String) = _uiState.update { it.copy(password = password) } + } + + data class UiState( + val username: String = "", + val password: String = "" + ) + + @Composable + fun LoginForm( + loginViewModel: LoginViewModel, + modifier: Modifier = Modifier + ) { + val initialUiState = remember(loginViewModel) { loginViewModel.uiState.value } + Column(modifier) { + val usernameState = rememberTextFieldState(initialUiState.username) + LaunchedEffect(usernameState) { + snapshotFlow { usernameState.text.toString() }.collectLatest { + loginViewModel.updateUsername(it) + } + } + TextField(usernameState) + + val passwordState = rememberTextFieldState(initialUiState.password) + LaunchedEffect(usernameState) { + snapshotFlow { usernameState.text.toString() }.collectLatest { + loginViewModel.updatePassword(it) + } + } + SecureTextField(passwordState) + } + } + // [END android_compose_text_textfield_migration_new_viewmodel_conforming] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt new file mode 100644 index 000000000..5973733a5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/TextSnippets.kt @@ -0,0 +1,963 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "UNUSED_VARIABLE", "PreviewMustBeTopLevelFunction") + +package com.example.compose.snippets.text + +import android.graphics.Typeface +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicSecureTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.TextObfuscationMode +import androidx.compose.foundation.text.selection.DisableSelection +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.Cyan +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.Hyphens +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel + +/** + * This file lets DevRel track changes to snippets present in + * https://developer.android.com/jetpack/compose/text + * + * No action required if it's modified. + */ + +private object SimpleTextSnippet { + // [START android_compose_text_simple_text] + @Composable + fun SimpleText() { + Text("Hello World") + } + // [END android_compose_text_simple_text] +} + +private object StringResourceSnippet { + // [START android_compose_text_resource] + @Composable + fun StringResourceText() { + Text(stringResource(R.string.hello_world)) + } + // [END android_compose_text_resource] +} + +private object TextColorSnippet { + // [START android_compose_text_color] + @Composable + fun BlueText() { + Text("Hello World", color = Color.Blue) + } + // [END android_compose_text_color] +} + +private object TextSizeSnippet { + // [START android_compose_text_size] + @Composable + fun BigText() { + Text("Hello World", fontSize = 30.sp) + } + // [END android_compose_text_size] +} + +private object TextItalicSnippet { + // [START android_compose_text_italic] + @Composable + fun ItalicText() { + Text("Hello World", fontStyle = FontStyle.Italic) + } + // [END android_compose_text_italic] +} + +private object TextBoldSnippet { + // [START android_compose_text_bold] + @Composable + fun BoldText() { + Text("Hello World", fontWeight = FontWeight.Bold) + } + // [END android_compose_text_bold] +} + +private object TextAlignmentSnippet { + // [START android_compose_text_alignment] + @Composable + fun CenterText() { + Text( + "Hello World", textAlign = TextAlign.Center, modifier = Modifier.width(150.dp) + ) + } + // [END android_compose_text_alignment] +} + +private object TextShadowSnippet { + // [START android_compose_text_shadow] + @Composable + fun TextShadow() { + val offset = Offset(5.0f, 10.0f) + Text( + text = "Hello world!", + style = TextStyle( + fontSize = 24.sp, + shadow = Shadow( + color = Color.Blue, offset = offset, blurRadius = 3f + ) + ) + ) + } + // [END android_compose_text_shadow] +} + +private object TextMultipleFontsSnippet { + // [START android_compose_text_multiple_fonts] + @Composable + fun DifferentFonts() { + Column { + Text("Hello World", fontFamily = FontFamily.Serif) + Text("Hello World", fontFamily = FontFamily.SansSerif) + } + } + // [END android_compose_text_multiple_fonts] +} + +@Composable +private fun TextDefineFontFamilySnippet() { + // [START android_compose_text_multiple_fonts_styles] + val firaSansFamily = FontFamily( + Font(R.font.firasans_light, FontWeight.Light), + Font(R.font.firasans_regular, FontWeight.Normal), + Font(R.font.firasans_italic, FontWeight.Normal, FontStyle.Italic), + Font(R.font.firasans_medium, FontWeight.Medium), + Font(R.font.firasans_bold, FontWeight.Bold) + ) + // [END android_compose_text_multiple_fonts_styles] +} + +@Composable +private fun TextFontWeightSnippet() { + // [START android_compose_text_font_weight] + Column { + Text(text = "text", fontFamily = firaSansFamily, fontWeight = FontWeight.Light) + Text(text = "text", fontFamily = firaSansFamily, fontWeight = FontWeight.Normal) + Text( + text = "text", + fontFamily = firaSansFamily, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Italic + ) + Text(text = "text", fontFamily = firaSansFamily, fontWeight = FontWeight.Medium) + Text(text = "text", fontFamily = firaSansFamily, fontWeight = FontWeight.Bold) + } + // [END android_compose_text_font_weight] +} + +private object TextMultipleStylesSnippet { + // [START android_compose_text_multistyle] + @Composable + fun MultipleStylesInText() { + Text( + buildAnnotatedString { + withStyle(style = SpanStyle(color = Color.Blue)) { + append("H") + } + append("ello ") + + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, color = Color.Red)) { + append("W") + } + append("orld") + } + ) + } + // [END android_compose_text_multistyle] +} + +private object TextParagraphStyleSnippet { + // [START android_compose_text_paragraph_style] + @Composable + fun ParagraphStyle() { + Text( + buildAnnotatedString { + withStyle(style = ParagraphStyle(lineHeight = 30.sp)) { + withStyle(style = SpanStyle(color = Color.Blue)) { + append("Hello\n") + } + withStyle( + style = SpanStyle( + fontWeight = FontWeight.Bold, color = Color.Red + ) + ) { + append("World\n") + } + append("Compose") + } + } + ) + } + // [END android_compose_text_paragraph_style] +} + +private object TextMaxLinesSnippet { + // [START android_compose_text_maxlines] + @Composable + fun LongText() { + Text("hello ".repeat(50), maxLines = 2) + } + // [END android_compose_text_maxlines] +} + +private object TextOverflowSnippet { + // [START android_compose_text_overflow] + @Composable + fun OverflowedText() { + Text("Hello Compose ".repeat(50), maxLines = 2, overflow = TextOverflow.Ellipsis) + } + // [END android_compose_text_overflow] +} + +private object TextBrushSnippet1 { + @Composable + fun TextStyledBrushSnippet(text: String) { + // [START android_compose_text_brush] + val gradientColors = listOf(Cyan, LightBlue, Purple /*...*/) + + Text( + text = text, + style = TextStyle( + brush = Brush.linearGradient( + colors = gradientColors + ) + ) + ) + // [END android_compose_text_brush] + } +} + +private object TextBrushSnippet2 { + @Composable + fun TextStyledBrushSnippet() { + val rainbowColors: List = listOf() + // [START android_compose_text_textfield_brush] + var text by remember { mutableStateOf("") } + val brush = remember { + Brush.linearGradient( + colors = rainbowColors + ) + } + TextField( + value = text, onValueChange = { text = it }, textStyle = TextStyle(brush = brush) + ) + // [END android_compose_text_textfield_brush] + } +} + +private object TextBrushSnippet3 { + @Composable + fun TextStyledBrushSnippet() { + val rainbowColors: List = listOf() + // [START android_compose_text_annotatedString_brush] + Text( + text = buildAnnotatedString { + append("Do not allow people to dim your shine\n") + withStyle( + SpanStyle( + brush = Brush.linearGradient( + colors = rainbowColors + ) + ) + ) { + append("because they are blinded.") + } + append("\nTell them to put some sunglasses on.") + } + ) + // [END android_compose_text_annotatedString_brush] + } +} + +private object TextBrushSnippet4 { + @Composable + fun TextStyledBrushSnippet() { + val rainbowColors: List = listOf() + // [START android_compose_text_alpha_brush] + val brush = Brush.linearGradient(colors = rainbowColors) + + buildAnnotatedString { + withStyle( + SpanStyle( + brush = brush, alpha = .5f + ) + ) { + append("Text in ") + } + withStyle( + SpanStyle( + brush = brush, alpha = 1f + ) + ) { + append("Compose ❤️") + } + } + // [END android_compose_text_alpha_brush] + } +} + +private object TextIncludeFontPaddingSnippet { + @Composable + fun AlignedText(text: String) { + // [START android_compose_text_includeFontPadding] + Text( + text = text, + style = LocalTextStyle.current.merge( + TextStyle( + lineHeight = 2.5.em, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ) + ) + ) + ) + // [END android_compose_text_includeFontPadding] + } +} + +private object TextSelectableSnippet { + // [START android_compose_text_selection] + @Composable + fun SelectableText() { + SelectionContainer { + Text("This text is selectable") + } + } + // [END android_compose_text_selection] +} + +private object TextPartiallySelectableSnippet { + // [START android_compose_text_partial_selection] + @Composable + fun PartiallySelectableText() { + SelectionContainer { + Column { + Text("This text is selectable") + Text("This one too") + Text("This one as well") + DisableSelection { + Text("But not this one") + Text("Neither this one") + } + Text("But again, you can select this one") + Text("And this one too") + } + } + } + // [END android_compose_text_partial_selection] +} + +private object TextTextFieldSnippet { + // [START android_compose_text_textfield_filled] + @Composable + fun SimpleFilledTextFieldSample() { + var text by remember { mutableStateOf("Hello") } + + TextField( + value = text, + onValueChange = { text = it }, + label = { Text("Label") } + ) + } + // [END android_compose_text_textfield_filled] +} + +private object TextOutlinedTextFieldSnippet { + // [START android_compose_text_textfield_outlined] + @Composable + fun SimpleOutlinedTextFieldSample() { + var text by remember { mutableStateOf("") } + + OutlinedTextField( + value = text, + onValueChange = { text = it }, + label = { Text("Label") } + ) + } + // [END android_compose_text_textfield_outlined] +} + +private object TextStylingTextFieldSnippet { + // [START android_compose_text_textfield_styled] + @Composable + fun StyledTextField() { + var value by remember { mutableStateOf("Hello\nWorld\nInvisible") } + + TextField( + value = value, + onValueChange = { value = it }, + label = { Text("Enter text") }, + maxLines = 2, + textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold), + modifier = Modifier.padding(20.dp) + ) + } + // [END android_compose_text_textfield_styled] +} + +private object TextFormattingTextFieldSnippet { + // [START android_compose_text_textfield_visualtransformation] + @Composable + fun PasswordTextField() { + var password by rememberSaveable { mutableStateOf("") } + + TextField( + value = password, + onValueChange = { password = it }, + label = { Text("Enter password") }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + } + // [END android_compose_text_textfield_visualtransformation] +} + +private object TextCleanInputSnippet { + // [START android_compose_text_textfield_clean_input] + @Composable + fun NoLeadingZeroes() { + var input by rememberSaveable { mutableStateOf("") } + TextField( + value = input, + onValueChange = { newText -> + input = newText.trimStart { it == '0' } + } + ) + } + // [END android_compose_text_textfield_clean_input] +} + +/** Effective State management **/ + +private object TextEffectiveStateManagement1 { + // [START android_compose_text_state_viewmodel] + class SignUpViewModel : ViewModel() { + + var username by mutableStateOf("") + private set + + /* ... */ + } + // [END android_compose_text_state_viewmodel] +} + +private object TextEffectiveStateManagement2 { + class UserRepository + + private val viewModel = SignUpViewModel(UserRepository()) + + // [START android_compose_text_state_management] + // SignUpViewModel.kt + + class SignUpViewModel(private val userRepository: UserRepository) : ViewModel() { + + var username by mutableStateOf("") + private set + + fun updateUsername(input: String) { + username = input + } + } + + // SignUpScreen.kt + + @Composable + fun SignUpScreen(/*...*/) { + + OutlinedTextField( + value = viewModel.username, + onValueChange = { username -> viewModel.updateUsername(username) } + /*...*/ + ) + } + // [END android_compose_text_state_management] +} + +// [START android_compose_text_link_1] +@Composable +fun AnnotatedStringWithLinkSample() { + // Display multiple links in the text + Text( + buildAnnotatedString { + append("Go to the ") + withLink( + LinkAnnotation.Url( + "https://developer.android.com/", + TextLinkStyles(style = SpanStyle(color = Color.Blue)) + ) + ) { + append("Android Developers ") + } + append("website, and check out the") + withLink( + LinkAnnotation.Url( + "https://developer.android.com/jetpack/compose", + TextLinkStyles(style = SpanStyle(color = Color.Green)) + ) + ) { + append("Compose guidance") + } + append(".") + } + ) +} +// [END android_compose_text_link_1] + +// [START android_compose_text_link_2] +@Composable +fun AnnotatedStringWithListenerSample() { + // Display a link in the text and log metrics whenever user clicks on it. In that case we handle + // the link using openUri method of the LocalUriHandler + val uriHandler = LocalUriHandler.current + Text( + buildAnnotatedString { + append("Build better apps faster with ") + val link = + LinkAnnotation.Url( + "https://developer.android.com/jetpack/compose", + TextLinkStyles(SpanStyle(color = Color.Blue)) + ) { + val url = (it as LinkAnnotation.Url).url + // log some metrics + uriHandler.openUri(url) + } + withLink(link) { append("Jetpack Compose") } + } + ) +} +// [END android_compose_text_link_2] + +@Composable +private fun TextSample(samples: Map Unit>) { + MaterialTheme { + Box( + Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth() + ) { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.padding(10.dp) + ) { + samples.forEach { (title, content) -> + Row(Modifier.fillMaxWidth()) { + content() + Text( + text = title, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterVertically) + ) + } + } + } + } + } +} + +private const val SAMPLE_LONG_TEXT = + "Jetpack Compose is Android’s modern toolkit for building native UI. " + + "It simplifies and accelerates UI development on Android bringing your apps " + + "to life with less code, powerful tools, and intuitive Kotlin APIs. " + + "It makes building Android UI faster and easier." + +@Composable +@Preview +fun LineBreakSample() { + // [START android_compose_text_line_break] + TextSample( + samples = mapOf( + "Simple" to { + Text( + text = SAMPLE_LONG_TEXT, + modifier = Modifier + .width(130.dp) + .border(BorderStroke(1.dp, Color.Gray)), + fontSize = 14.sp, + style = TextStyle.Default.copy( + lineBreak = LineBreak.Simple + ) + ) + }, + "Paragraph" to { + Text( + text = SAMPLE_LONG_TEXT, + modifier = Modifier + .width(130.dp) + .border(BorderStroke(1.dp, Color.Gray)), + fontSize = 14.sp, + style = TextStyle.Default.copy( + lineBreak = LineBreak.Paragraph + ) + ) + } + ) + ) + // [END android_compose_text_line_break] +} + +@Preview +@Composable +fun SmallScreenTextSnippet() { + // [START android_compose_text_paragraph] + TextSample( + samples = mapOf( + "Balanced" to { + val smallScreenAdaptedParagraph = + LineBreak.Paragraph.copy(strategy = LineBreak.Strategy.Balanced) + Text( + text = SAMPLE_LONG_TEXT, + modifier = Modifier + .width(200.dp) + .border(BorderStroke(1.dp, Color.Gray)), + fontSize = 14.sp, + style = TextStyle.Default.copy( + lineBreak = smallScreenAdaptedParagraph + ) + ) + }, + "Default" to { + Text( + text = SAMPLE_LONG_TEXT, + modifier = Modifier + .width(200.dp) + .border(BorderStroke(1.dp, Color.Gray)), + fontSize = 14.sp, + style = TextStyle.Default + ) + } + ) + ) + // [END android_compose_text_paragraph] +} + +private object CJKTextSnippet { + @Composable + fun CJKSample() { + // [START android_compose_text_cjk] + val customTitleLineBreak = LineBreak( + strategy = LineBreak.Strategy.HighQuality, + strictness = LineBreak.Strictness.Strict, + wordBreak = LineBreak.WordBreak.Phrase + ) + Text( + text = "あなたに寄り添う最先端のテクノロジー。", + modifier = Modifier.width(250.dp), + fontSize = 14.sp, + style = TextStyle.Default.copy( + lineBreak = customTitleLineBreak + ) + ) + // [END android_compose_text_cjk] + } +} + +@Preview +@Composable +fun HyphenateTextSnippet() { + // [START android_compose_text_hyphen] + TextSample( + samples = mapOf( + "Hyphens - None" to { + Text( + text = SAMPLE_LONG_TEXT, + modifier = Modifier + .width(130.dp) + .border(BorderStroke(1.dp, Color.Gray)), + fontSize = 14.sp, + style = TextStyle.Default.copy( + lineBreak = LineBreak.Paragraph, + hyphens = Hyphens.None + ) + ) + }, + "Hyphens - Auto" to { + Text( + text = SAMPLE_LONG_TEXT, + modifier = Modifier + .width(130.dp) + .border(BorderStroke(1.dp, Color.Gray)), + fontSize = 14.sp, + style = TextStyle.Default.copy( + lineBreak = LineBreak.Paragraph, + hyphens = Hyphens.Auto + ) + ) + } + ) + ) + // [END android_compose_text_hyphen] +} + +@Preview(showBackground = true) +// [START android_compose_text_marquee] +@Composable +fun BasicMarqueeSample() { + // Marquee only animates when the content doesn't fit in the max width. + Column(Modifier.width(400.dp)) { + Text( + "Learn about why it's great to use Jetpack Compose", + modifier = Modifier.basicMarquee(), + fontSize = 50.sp + ) + } +} +// [END android_compose_text_marquee] + +// [START android_compose_text_auto_format_phone_number_textfieldconfig] +@Composable +fun PhoneNumber() { + var phoneNumber by rememberSaveable { mutableStateOf("") } + val numericRegex = Regex("[^0-9]") + TextField( + value = phoneNumber, + onValueChange = { + // Remove non-numeric characters. + val stripped = numericRegex.replace(it, "") + phoneNumber = if (stripped.length >= 10) { + stripped.substring(0..9) + } else { + stripped + } + }, + label = { Text("Enter Phone Number") }, + visualTransformation = NanpVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) +} +// [END android_compose_text_auto_format_phone_number_textfieldconfig] + +// [START android_compose_text_auto_format_phone_number_transformtext] +class NanpVisualTransformation : VisualTransformation { + + override fun filter(text: AnnotatedString): TransformedText { + val trimmed = if (text.text.length >= 10) text.text.substring(0..9) else text.text + + var out = if (trimmed.isNotEmpty()) "(" else "" + + for (i in trimmed.indices) { + if (i == 3) out += ") " + if (i == 6) out += "-" + out += trimmed[i] + } + return TransformedText(AnnotatedString(out), phoneNumberOffsetTranslator) + } + + private val phoneNumberOffsetTranslator = object : OffsetMapping { + + override fun originalToTransformed(offset: Int): Int = + when (offset) { + 0 -> offset + // Add 1 for opening parenthesis. + in 1..3 -> offset + 1 + // Add 3 for both parentheses and a space. + in 4..6 -> offset + 3 + // Add 4 for both parentheses, space, and hyphen. + else -> offset + 4 + } + + override fun transformedToOriginal(offset: Int): Int = + when (offset) { + 0 -> offset + // Subtract 1 for opening parenthesis. + in 1..5 -> offset - 1 + // Subtract 3 for both parentheses and a space. + in 6..10 -> offset - 3 + // Subtract 4 for both parentheses, space, and hyphen. + else -> offset - 4 + } + } +} +// [END android_compose_text_auto_format_phone_number_transformtext] + +private val firaSansFamily = FontFamily(typeface = Typeface.DEFAULT) + +val LightBlue = Color(0xFF0066FF) +val Purple = Color(0xFF800080) + +// [START android_compose_text_showhidepassword] +@Composable +fun PasswordTextField() { + val state = remember { TextFieldState() } + var showPassword by remember { mutableStateOf(false) } + BasicSecureTextField( + state = state, + textObfuscationMode = + if (showPassword) { + TextObfuscationMode.Visible + } else { + TextObfuscationMode.RevealLastTyped + }, + modifier = Modifier + .fillMaxWidth() + .padding(6.dp) + .border(1.dp, Color.LightGray, RoundedCornerShape(6.dp)) + .padding(6.dp), + decorator = { innerTextField -> + Box(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 16.dp, end = 48.dp) + ) { + innerTextField() + } + Icon( + if (showPassword) { + Icons.Filled.Visibility + } else { + Icons.Filled.VisibilityOff + }, + contentDescription = "Toggle password visibility", + modifier = Modifier + .align(Alignment.CenterEnd) + .requiredSize(48.dp).padding(16.dp) + .clickable { showPassword = !showPassword } + ) + } + } + ) +} +// [END android_compose_text_showhidepassword] + +// [START android_compose_text_auto_format_phone_number_validatetext] +class EmailViewModel : ViewModel() { + var email by mutableStateOf("") + private set + + val emailHasErrors by derivedStateOf { + if (email.isNotEmpty()) { + // Email is considered erroneous until it completely matches EMAIL_ADDRESS. + !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() + } else { + false + } + } + + fun updateEmail(input: String) { + email = input + } +} + +@Composable +fun ValidatingInputTextField( + email: String, + updateState: (String) -> Unit, + validatorHasErrors: Boolean +) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = email, + onValueChange = updateState, + label = { Text("Email") }, + isError = validatorHasErrors, + supportingText = { + if (validatorHasErrors) { + Text("Incorrect email format.") + } + } + ) +} + +@Preview +@Composable +fun ValidateInput() { + val emailViewModel: EmailViewModel = viewModel() + ValidatingInputTextField( + email = emailViewModel.email, + updateState = { input -> emailViewModel.updateEmail(input) }, + validatorHasErrors = emailViewModel.emailHasErrors + ) +} +// [END android_compose_text_auto_format_phone_number_validatetext] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/VariableFontsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/VariableFontsSnippets.kt new file mode 100644 index 000000000..80b8361fe --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/VariableFontsSnippets.kt @@ -0,0 +1,511 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused", "PreviewMustBeTopLevelFunction") +package com.example.compose.snippets.text + +import android.os.Build +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontVariation +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.R + +private object VariableFontSnippet1 { + // [START android_compose_text_variable_fonts_define] + // In Typography.kt + @OptIn(ExperimentalTextApi::class) + val displayLargeFontFamily = + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(950), + FontVariation.width(30f), + FontVariation.slant(-6f), + ) + ) + ) + // [END android_compose_text_variable_fonts_define] +} +private object VariableFontDefaultSnippet2 { + // [START android_compose_text_variable_fonts_fallback] + // In Typography.kt + val default = FontFamily( + /* + * This can be any font that makes sense + */ + Font( + R.font.robotoflex_static_regular + ) + ) + @OptIn(ExperimentalTextApi::class) + val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(950), + FontVariation.width(30f), + FontVariation.slant(-6f), + ) + ) + ) + } else { + default + } + // [END android_compose_text_variable_fonts_fallback] +} + +private object VariableFontSnippetsExtractedSettings { + // In Typography.kt + val default = FontFamily( + /* + * This can be any font that makes sense + */ + Font( + R.font.robotoflex_static_regular + ) + ) + // [START android_compose_text_variable_fonts_extracted_settings] + // VariableFontDimension.kt + object DisplayLargeVFConfig { + const val WEIGHT = 950 + const val WIDTH = 30f + const val SLANT = -6f + const val ASCENDER_HEIGHT = 800f + const val COUNTER_WIDTH = 500 + } + + @OptIn(ExperimentalTextApi::class) + val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(DisplayLargeVFConfig.WEIGHT), + FontVariation.width(DisplayLargeVFConfig.WIDTH), + FontVariation.slant(DisplayLargeVFConfig.SLANT), + ) + ) + ) + } else { + default + } + // [END android_compose_text_variable_fonts_extracted_settings] +} +private object VariableFontSnippetsUseTheme { + // In Typography.kt + val default = FontFamily( + /* + * This can be any font that makes sense + */ + Font( + R.font.robotoflex_static_regular + ) + ) + @OptIn(ExperimentalTextApi::class) + val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(VariableFontSnippetsExtractedSettings.DisplayLargeVFConfig.WEIGHT), + FontVariation.width(VariableFontSnippetsExtractedSettings.DisplayLargeVFConfig.WIDTH), + FontVariation.slant(VariableFontSnippetsExtractedSettings.DisplayLargeVFConfig.SLANT), + ) + ) + ) + } else { + default + } + // [START android_compose_text_variable_fonts_typography] + // Type.kt + val Typography = Typography( + displayLarge = TextStyle( + fontFamily = displayLargeFontFamily, + fontSize = 50.sp, + lineHeight = 64.sp, + letterSpacing = 0.sp, + /***/ + ) + ) + // [END android_compose_text_variable_fonts_typography] +} + +private object VariableFontsTypographyTheme { + object DisplayLargeVFConfig { + const val WEIGHT = 950 + const val WIDTH = 30f + const val SLANT = -6f + const val ASCENDER_HEIGHT = 800f + const val COUNTER_WIDTH = 500 + } + + object HeadlineMediumVFConfig { + const val WEIGHT = 800 + const val WIDTH = 90f + const val SLANT = 0f + const val ASCENDER_HEIGHT = 750f + const val COUNTER_WIDTH = 393 + } + + object BodyLargeVFConfig { + const val WEIGHT = 400 + const val WIDTH = 50f + const val SLANT = 0f + const val ASCENDER_HEIGHT = 750f + const val COUNTER_WIDTH = 603 + } + // In Typography.kt + val default = FontFamily( + /* + * This can be any font that makes sense + */ + Font( + R.font.robotoflex_static_regular + ) + ) + @OptIn(ExperimentalTextApi::class) + val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(VariableFontSnippetsExtractedSettings.DisplayLargeVFConfig.WEIGHT), + FontVariation.width(VariableFontSnippetsExtractedSettings.DisplayLargeVFConfig.WIDTH), + FontVariation.slant(VariableFontSnippetsExtractedSettings.DisplayLargeVFConfig.SLANT), + ) + ) + ) + } else { + default + } + @OptIn(ExperimentalTextApi::class) + val headlineMediumFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(HeadlineMediumVFConfig.WEIGHT), + FontVariation.width(HeadlineMediumVFConfig.WIDTH), + FontVariation.slant(HeadlineMediumVFConfig.SLANT) + ) + ) + ) + } else { + default + } + + @OptIn(ExperimentalTextApi::class) + val bodyLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(BodyLargeVFConfig.WEIGHT), + FontVariation.width(BodyLargeVFConfig.WIDTH), + FontVariation.slant(BodyLargeVFConfig.SLANT) + ) + ) + ) + } else { + FontFamily( + Font( + R.font.robotoflex_static_regular + ) + ) + } + // Type.kt + val Typography = Typography( + displayLarge = TextStyle( + fontFamily = displayLargeFontFamily, + fontSize = 50.sp, + lineHeight = 64.sp, + letterSpacing = 0.sp, + /***/ + ), + headlineMedium = TextStyle( + fontFamily = headlineMediumFontFamily, + fontSize = 35.sp, + lineHeight = 37.sp + /***/ + ), + bodyLarge = TextStyle( + fontFamily = bodyLargeFontFamily, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + lineHeight = 28.sp, + letterSpacing = 0.15.sp + /***/ + ), + ) + + @Composable + fun MyCustomTheme( + content: @Composable () -> Unit, + ) { + // [START android_compose_variable_fonts_use_theme] + MaterialTheme( + colorScheme = MaterialTheme.colorScheme, + typography = Typography, + content = content + ) + // [END android_compose_variable_fonts_use_theme] + } + + // [START android_compose_variable_fonts_usage] + @Composable + @Preview + fun CardDetails() { + MyCustomTheme { + Card( + shape = RoundedCornerShape(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "Compose", + style = MaterialTheme.typography.displayLarge, + modifier = Modifier.padding(bottom = 8.dp), + maxLines = 1 + ) + Text( + text = "Beautiful UIs on Android", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 8.dp), + maxLines = 2 + ) + Text( + text = "Jetpack Compose is Android’s recommended modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(bottom = 8.dp), + maxLines = 3 + ) + } + } + } + } + // [END android_compose_variable_fonts_usage] +} + +private object VariableFontsTypographyCustomAxes { + // [START android_compose_variable_font_custom_axes] + fun ascenderHeight(ascenderHeight: Float): FontVariation.Setting { + require(ascenderHeight in 649f..854f) { "'Ascender Height' must be in 649f..854f" } + return FontVariation.Setting("YTAS", ascenderHeight) + } + + fun counterWidth(counterWidth: Int): FontVariation.Setting { + require(counterWidth in 323..603) { "'Counter width' must be in 323..603" } + return FontVariation.Setting("XTRA", counterWidth.toFloat()) + } + // [END android_compose_variable_font_custom_axes] + object DisplayLargeVFConfig { + const val WEIGHT = 950 + const val WIDTH = 30f + const val SLANT = -6f + const val ASCENDER_HEIGHT = 800f + const val COUNTER_WIDTH = 500 + } + + object HeadlineMediumVFConfig { + const val WEIGHT = 800 + const val WIDTH = 90f + const val SLANT = 0f + const val ASCENDER_HEIGHT = 750f + const val COUNTER_WIDTH = 393 + } + + object BodyLargeVFConfig { + const val WEIGHT = 400 + const val WIDTH = 50f + const val SLANT = 0f + const val ASCENDER_HEIGHT = 750f + const val COUNTER_WIDTH = 603 + } + // In Typography.kt + val default = FontFamily( + /* + * This can be any font that makes sense + */ + Font( + R.font.robotoflex_static_regular + ) + ) + // [START android_compose_variable_font_custom_axis_usage] + @OptIn(ExperimentalTextApi::class) + val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(DisplayLargeVFConfig.WEIGHT), + FontVariation.width(DisplayLargeVFConfig.WIDTH), + FontVariation.slant(DisplayLargeVFConfig.SLANT), + ascenderHeight(DisplayLargeVFConfig.ASCENDER_HEIGHT), + counterWidth(DisplayLargeVFConfig.COUNTER_WIDTH) + ) + ) + ) + } else { + default + } + // [END android_compose_variable_font_custom_axis_usage] + + @OptIn(ExperimentalTextApi::class) + val headlineMediumFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(HeadlineMediumVFConfig.WEIGHT), + FontVariation.width(HeadlineMediumVFConfig.WIDTH), + FontVariation.slant(HeadlineMediumVFConfig.SLANT), + ascenderHeight(HeadlineMediumVFConfig.ASCENDER_HEIGHT), + counterWidth(HeadlineMediumVFConfig.COUNTER_WIDTH) + ) + ) + ) + } else { + default + } + + @OptIn(ExperimentalTextApi::class) + val bodyLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FontFamily( + Font( + R.font.robotoflex_variable, + variationSettings = FontVariation.Settings( + FontVariation.weight(BodyLargeVFConfig.WEIGHT), + FontVariation.width(BodyLargeVFConfig.WIDTH), + FontVariation.slant(BodyLargeVFConfig.SLANT), + ascenderHeight(BodyLargeVFConfig.ASCENDER_HEIGHT), + counterWidth(BodyLargeVFConfig.COUNTER_WIDTH) + ) + ) + ) + } else { + FontFamily( + Font( + R.font.robotoflex_static_regular + ) + ) + } + // Type.kt + val Typography = Typography( + displayLarge = TextStyle( + fontFamily = displayLargeFontFamily, + fontSize = 50.sp, + lineHeight = 64.sp, + letterSpacing = 0.sp, + /***/ + ), + headlineMedium = TextStyle( + fontFamily = headlineMediumFontFamily, + fontSize = 35.sp, + lineHeight = 37.sp + /***/ + ), + bodyLarge = TextStyle( + fontFamily = bodyLargeFontFamily, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + lineHeight = 28.sp, + letterSpacing = 0.15.sp + /***/ + ), + ) + + @Composable + fun MyCustomTheme( + content: @Composable () -> Unit, + ) { + MaterialTheme( + colorScheme = MaterialTheme.colorScheme, + typography = Typography, + content = content + ) + } + + @Composable + @Preview + fun CardDetails() { + MyCustomTheme { + Card( + shape = RoundedCornerShape(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "Compose", + style = MaterialTheme.typography.displayLarge, + modifier = Modifier.padding(bottom = 8.dp), + maxLines = 1 + ) + Text( + text = "Beautiful UIs on Android.", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 8.dp), + maxLines = 2 + ) + Text( + text = "Jetpack Compose is Android’s recommended modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(bottom = 8.dp), + maxLines = 3 + ) + } + } + } + } +} +@Composable +@Preview +fun VariableFontConfigured() { + VariableFontsTypographyTheme.CardDetails() +} +@Composable +@Preview +fun VariableFontConfiguredCustomAxes() { + VariableFontsTypographyCustomAxes.CardDetails() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/tooling/AndroidStudioComposeSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/tooling/AndroidStudioComposeSnippets.kt new file mode 100644 index 000000000..d1c23a52d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/tooling/AndroidStudioComposeSnippets.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.tooling + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.example.compose.snippets.R +import com.example.compose.snippets.interop.User + +// [START android_compose_tooling_simple_composable] +@Composable +fun SimpleComposable() { + Text("Hello World") +} +// [END android_compose_tooling_simple_composable] + +// [START android_compose_tooling_simple_composable_preview] +@Preview +@Composable +fun SimpleComposablePreview() { + SimpleComposable() +} +// [END android_compose_tooling_simple_composable_preview] + +// [START android_compose_tooling_local_inspection_mode] +@Composable +fun GreetingScreen(name: String) { + if (LocalInspectionMode.current) { + // Show this text in a preview window: + Text("Hello preview user!") + } else { + // Show this text in the app: + Text("Hello $name!") + } +} +// [END android_compose_tooling_local_inspection_mode] + +// [START android_compose_tooling_multipreview_annotations] +@Preview( + name = "small font", + group = "font scales", + fontScale = 0.5f +) +@Preview( + name = "large font", + group = "font scales", + fontScale = 1.5f +) +annotation class FontScalePreviews +// [END android_compose_tooling_multipreview_annotations] + +// [START android_compose_tooling_multipreview_usage] +@FontScalePreviews +@Composable +fun HelloWorldPreview() { + Text("Hello World") +} +// [END android_compose_tooling_multipreview_usage] + +// [START android_compose_tooling_multipreview_combine] +@Preview( + name = "Spanish", + group = "locale", + locale = "es" +) +@FontScalePreviews +annotation class CombinedPreviews + +@CombinedPreviews +@Composable +fun HelloWorldPreview2() { + MaterialTheme { Surface { Text(stringResource(R.string.hello_world)) } } +} +// [END android_compose_tooling_multipreview_combine] + +// [START android_compose_tooling_preview_bg_color] +@Preview(showBackground = true, backgroundColor = 0xFF00FF00) +@Composable +fun WithGreenBackground() { + Text("Hello World") +} +// [END android_compose_tooling_preview_bg_color] + +// [START android_compose_tooling_preview_dimens] +@Preview(widthDp = 50, heightDp = 50) +@Composable +fun SquareComposablePreview() { + Box(Modifier.background(Color.Yellow)) { + Text("Hello World") + } +} +// [END android_compose_tooling_preview_dimens] + +// [START android_compose_tooling_preview_locale] +@Preview(locale = "fr-rFR") +@Composable +fun DifferentLocaleComposablePreview() { + Text(text = stringResource(R.string.greeting)) +} +// [END android_compose_tooling_preview_locale] + +// [START android_compose_tooling_preview_system_ui] +@Preview(showSystemUi = true) +@Composable +fun DecoratedComposablePreview() { + Text("Hello World") +} +// [END android_compose_tooling_preview_system_ui] + +// [START android_compose_tooling_preview_parameter_provider_composable] +@Preview +@Composable +fun UserProfilePreview( + @PreviewParameter(UserPreviewParameterProvider::class) user: User +) { + UserProfile(user) +} + +// [START_EXCLUDE silent] +@Composable +fun UserProfile(user: User) { +} +// [END_EXCLUDE] +// [END android_compose_tooling_preview_parameter_provider_composable] + +// [START android_compose_tooling_preview_parameter_provider] +class UserPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + User("Elise"), + User("Frank"), + User("Julia") + ) +} +// [END android_compose_tooling_preview_parameter_provider] + +// [START android_compose_tooling_preview_parameter_provider_composable2] +@Preview +@Composable +fun UserProfilePreview2( + @PreviewParameter(UserPreviewParameterProvider::class, limit = 2) user: User +) { + UserProfile(user) +} +// [END android_compose_tooling_preview_parameter_provider_composable2] +/* +private object PreviewAnnotationEx { + // [START android_compose_tooling_preview_annotation] + annotation class Preview( + val name: String = "", + val group: String = "", + @IntRange(from = 1) val apiLevel: Int = -1, + val widthDp: Int = -1, + val heightDp: Int = -1, + val locale: String = "", + @FloatRange(from = 0.01) val fontScale: Float = 1f, + val showSystemUi: Boolean = false, + val showBackground: Boolean = false, + val backgroundColor: Long = 0, + @UiMode val uiMode: Int = 0, + @Device val device: String = Devices.DEFAULT, + @Wallpaper val wallpaper: Int = Wallpapers.NONE, + ) +// [END android_compose_tooling_preview_annotation] +} + */ diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/tooling/IterativeDevelopmentSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/tooling/IterativeDevelopmentSnippets.kt new file mode 100644 index 000000000..3baa9998f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/tooling/IterativeDevelopmentSnippets.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.tooling + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +// [START android_compose_tooling_iterative_develop_greeting] +@Composable +fun Greeting(name: String) { + Text( + text = "Hello $name!", + Modifier + .padding(80.dp) // Outer padding; outside background + .background(color = Color.Cyan) // Solid element background color + .padding(16.dp) // Inner padding; inside background, around text) + ) +} +// [END android_compose_tooling_iterative_develop_greeting] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt new file mode 100644 index 000000000..14b012da1 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/Interactions.kt @@ -0,0 +1,643 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.touchinput + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.IndicationNodeFactory +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material.ripple +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ButtonElevation +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Brush.Companion.linearGradient +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.abs +import kotlin.math.sign +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@Composable +private fun InteractionsSnippet1() { + // [START android_compose_interactions_interaction_state] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Button( + onClick = { /* do something */ }, + interactionSource = interactionSource + ) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } + // [END android_compose_interactions_interaction_state] +} + +// [START android_compose_interactions_interaction_source_input] +fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { + // [START_EXCLUDE] + return this + // [END_EXCLUDE] +} +// [END android_compose_interactions_interaction_source_input] + +// [START android_compose_interactions_mutable_interaction_source_input] +fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { + // [START_EXCLUDE] + return this + // [END_EXCLUDE] +} +// [END android_compose_interactions_mutable_interaction_source_input] + +// [START android_compose_interactions_high_level_component] +@Composable +fun Button( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + + // exposes MutableInteractionSource as a parameter + interactionSource: MutableInteractionSource? = null, + + elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), + shape: Shape = MaterialTheme.shapes.small, + border: BorderStroke? = null, + colors: ButtonColors = ButtonDefaults.buttonColors(), + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + content: @Composable RowScope.() -> Unit +) { /* content() */ } +// [END android_compose_interactions_high_level_component] + +@Composable +fun HoverExample() { + // [START android_compose_interactions_hoverable] + // This InteractionSource will emit hover interactions + val interactionSource = remember { MutableInteractionSource() } + + Box( + Modifier + .size(100.dp) + .hoverable(interactionSource = interactionSource), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_hoverable] +} + +@Composable +fun FocusableExample() { + // [START android_compose_interactions_focusable] + // This InteractionSource will emit hover and focus interactions + val interactionSource = remember { MutableInteractionSource() } + + Box( + Modifier + .size(100.dp) + .hoverable(interactionSource = interactionSource) + .focusable(interactionSource = interactionSource), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_focusable] +} + +@Composable +fun ClickableExample() { + // [START android_compose_interactions_clickable] + // This InteractionSource will emit hover, focus, and press interactions + val interactionSource = remember { MutableInteractionSource() } + Box( + Modifier + .size(100.dp) + .clickable( + onClick = {}, + interactionSource = interactionSource, + + // Also show a ripple effect + indication = ripple() + ), + contentAlignment = Alignment.Center + ) { + Text("Hello!") + } + // [END android_compose_interactions_clickable] +} + +@Composable +private fun InteractionsSnippet2() { + // [START android_compose_interactions_flow_apis] + val interactionSource = remember { MutableInteractionSource() } + val interactions = remember { mutableStateListOf() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> { + interactions.add(interaction) + } + is DragInteraction.Start -> { + interactions.add(interaction) + } + } + } + } + // [END android_compose_interactions_flow_apis] +} + +@Composable +private fun InteractionsSnippet3() { + // [START android_compose_interactions_add_remove] + val interactionSource = remember { MutableInteractionSource() } + val interactions = remember { mutableStateListOf() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> { + interactions.add(interaction) + } + is PressInteraction.Release -> { + interactions.remove(interaction.press) + } + is PressInteraction.Cancel -> { + interactions.remove(interaction.press) + } + is DragInteraction.Start -> { + interactions.add(interaction) + } + is DragInteraction.Stop -> { + interactions.remove(interaction.start) + } + is DragInteraction.Cancel -> { + interactions.remove(interaction.start) + } + } + } + } + // [END android_compose_interactions_add_remove] + + // [START android_compose_interactions_is_pressed_or_dragged] + val isPressedOrDragged = interactions.isNotEmpty() + // [END android_compose_interactions_is_pressed_or_dragged] + + // [START android_compose_interactions_last] + val lastInteraction = when (interactions.lastOrNull()) { + is DragInteraction.Start -> "Dragged" + is PressInteraction.Press -> "Pressed" + else -> "No state" + } + // [END android_compose_interactions_last] +} + +@Composable +private fun InteractionsSnippet4() { + // [START android_compose_interactions_batched] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Button(onClick = { /* do something */ }, interactionSource = interactionSource) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } + // [END android_compose_interactions_batched] +} + +// [START android_compose_interactions_press_icon_button] +@Composable +fun PressIconButton( + onClick: () -> Unit, + icon: @Composable () -> Unit, + text: @Composable () -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource? = null +) { + val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false + + Button( + onClick = onClick, + modifier = modifier, + interactionSource = interactionSource + ) { + AnimatedVisibility(visible = isPressed) { + if (isPressed) { + Row { + icon() + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + } + } + } + text() + } +} +// [END android_compose_interactions_press_icon_button] + +@Composable +fun PressIconButtonUsage() { +// [START android_compose_interactions_press_icon_button_usage] + PressIconButton( + onClick = {}, + icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, + text = { Text("Add to cart") } + ) +// [END android_compose_interactions_press_icon_button_usage] +} + +@Composable +fun InteractionsSnippet5() { +// [START android_compose_interactions_indication] + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale") + + Button( + modifier = Modifier.scale(scale), + onClick = { }, + interactionSource = interactionSource + ) { + Text(if (isPressed) "Pressed!" else "Not pressed") + } +// [END android_compose_interactions_indication] +} + +// [START android_compose_interactions_scale_node] +private class ScaleNode(private val interactionSource: InteractionSource) : + Modifier.Node(), DrawModifierNode { + + var currentPressPosition: Offset = Offset.Zero + val animatedScalePercent = Animatable(1f) + + private suspend fun animateToPressed(pressPosition: Offset) { + currentPressPosition = pressPosition + animatedScalePercent.animateTo(0.9f, spring()) + } + + private suspend fun animateToResting() { + animatedScalePercent.animateTo(1f, spring()) + } + + override fun onAttach() { + coroutineScope.launch { + interactionSource.interactions.collectLatest { interaction -> + when (interaction) { + is PressInteraction.Press -> animateToPressed(interaction.pressPosition) + is PressInteraction.Release -> animateToResting() + is PressInteraction.Cancel -> animateToResting() + } + } + } + } + + override fun ContentDrawScope.draw() { + scale( + scale = animatedScalePercent.value, + pivot = currentPressPosition + ) { + this@draw.drawContent() + } + } +} +// [END android_compose_interactions_scale_node] + +// [START android_compose_interactions_scale_node_factory] +object ScaleIndication : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return ScaleNode(interactionSource) + } + + override fun equals(other: Any?): Boolean = other === ScaleIndication + override fun hashCode() = 100 +} +// [END android_compose_interactions_scale_node_factory] + +@Composable +fun InteractionSnippets6() { +// [START android_compose_interactions_button_indication] + Box( + modifier = Modifier + .size(100.dp) + .clickable( + onClick = {}, + indication = ScaleIndication, + interactionSource = null + ) + .background(Color.Blue), + contentAlignment = Alignment.Center + ) { + Text("Hello!", color = Color.White) + } +// [END android_compose_interactions_button_indication] +} + +// [START android_compose_interactions_scale_button] +@Composable +fun ScaleButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + shape: Shape = CircleShape, + content: @Composable RowScope.() -> Unit +) { + Row( + modifier = modifier + .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp) + .clickable( + enabled = enabled, + indication = ScaleIndication, + interactionSource = interactionSource, + onClick = onClick + ) + .border(width = 2.dp, color = Color.Blue, shape = shape) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content + ) +} +// [END android_compose_interactions_scale_button] + +@Composable +fun ScaleButtonExample() { +// [START android_compose_interactions_scale_button_example] + ScaleButton(onClick = {}) { + Icon(Icons.Filled.ShoppingCart, "") + Spacer(Modifier.padding(10.dp)) + Text(text = "Add to cart!") + } +// [END android_compose_interactions_scale_button_example] +} + +// [START android_compose_interactions_neon_node] +private class NeonNode( + private val shape: Shape, + private val borderWidth: Dp, + private val interactionSource: InteractionSource +) : Modifier.Node(), DrawModifierNode { + var currentPressPosition: Offset = Offset.Zero + val animatedProgress = Animatable(0f) + val animatedPressAlpha = Animatable(1f) + + var pressedAnimation: Job? = null + var restingAnimation: Job? = null + + private suspend fun animateToPressed(pressPosition: Offset) { + // Finish any existing animations, in case of a new press while we are still showing + // an animation for a previous one + restingAnimation?.cancel() + pressedAnimation?.cancel() + pressedAnimation = coroutineScope.launch { + currentPressPosition = pressPosition + animatedPressAlpha.snapTo(1f) + animatedProgress.snapTo(0f) + animatedProgress.animateTo(1f, tween(450)) + } + } + + private fun animateToResting() { + restingAnimation = coroutineScope.launch { + // Wait for the existing press animation to finish if it is still ongoing + pressedAnimation?.join() + animatedPressAlpha.animateTo(0f, tween(250)) + animatedProgress.snapTo(0f) + } + } + + override fun onAttach() { + coroutineScope.launch { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> animateToPressed(interaction.pressPosition) + is PressInteraction.Release -> animateToResting() + is PressInteraction.Cancel -> animateToResting() + } + } + } + } + + override fun ContentDrawScope.draw() { + val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition( + currentPressPosition, size + ) + val brush = animateBrush( + startPosition = startPosition, + endPosition = endPosition, + progress = animatedProgress.value + ) + val alpha = animatedPressAlpha.value + + drawContent() + + val outline = shape.createOutline(size, layoutDirection, this) + // Draw overlay on top of content + drawOutline( + outline = outline, + brush = brush, + alpha = alpha * 0.1f + ) + // Draw border on top of overlay + drawOutline( + outline = outline, + brush = brush, + alpha = alpha, + style = Stroke(width = borderWidth.toPx()) + ) + } + + /** + * Calculates a gradient start / end where start is the point on the bounding rectangle of + * size [size] that intercepts with the line drawn from the center to [pressPosition], + * and end is the intercept on the opposite end of that line. + */ + private fun calculateGradientStartAndEndFromPressPosition( + pressPosition: Offset, + size: Size + ): Pair { + // Convert to offset from the center + val offset = pressPosition - size.center + // y = mx + c, c is 0, so just test for x and y to see where the intercept is + val gradient = offset.y / offset.x + // We are starting from the center, so halve the width and height - convert the sign + // to match the offset + val width = (size.width / 2f) * sign(offset.x) + val height = (size.height / 2f) * sign(offset.y) + val x = height / gradient + val y = gradient * width + + // Figure out which intercept lies within bounds + val intercept = if (abs(y) <= abs(height)) { + Offset(width, y) + } else { + Offset(x, height) + } + + // Convert back to offsets from 0,0 + val start = intercept + size.center + val end = Offset(size.width - start.x, size.height - start.y) + return start to end + } + + private fun animateBrush( + startPosition: Offset, + endPosition: Offset, + progress: Float + ): Brush { + if (progress == 0f) return TransparentBrush + + // This is *expensive* - we are doing a lot of allocations on each animation frame. To + // recreate a similar effect in a performant way, it would be better to create one large + // gradient and translate it on each frame, instead of creating a whole new gradient + // and shader. The current approach will be janky! + val colorStops = buildList { + when { + progress < 1 / 6f -> { + val adjustedProgress = progress * 6f + add(0f to Blue) + add(adjustedProgress to Color.Transparent) + } + progress < 2 / 6f -> { + val adjustedProgress = (progress - 1 / 6f) * 6f + add(0f to Purple) + add(adjustedProgress * MaxBlueStop to Blue) + add(adjustedProgress to Blue) + add(1f to Color.Transparent) + } + progress < 3 / 6f -> { + val adjustedProgress = (progress - 2 / 6f) * 6f + add(0f to Pink) + add(adjustedProgress * MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + progress < 4 / 6f -> { + val adjustedProgress = (progress - 3 / 6f) * 6f + add(0f to Orange) + add(adjustedProgress * MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + progress < 5 / 6f -> { + val adjustedProgress = (progress - 4 / 6f) * 6f + add(0f to Yellow) + add(adjustedProgress * MaxOrangeStop to Orange) + add(MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + else -> { + val adjustedProgress = (progress - 5 / 6f) * 6f + add(0f to Yellow) + add(adjustedProgress * MaxYellowStop to Yellow) + add(MaxOrangeStop to Orange) + add(MaxPinkStop to Pink) + add(MaxPurpleStop to Purple) + add(MaxBlueStop to Blue) + add(1f to Blue) + } + } + } + + return linearGradient( + colorStops = colorStops.toTypedArray(), + start = startPosition, + end = endPosition + ) + } + + companion object { + val TransparentBrush = SolidColor(Color.Transparent) + val Blue = Color(0xFF30C0D8) + val Purple = Color(0xFF7848A8) + val Pink = Color(0xFFF03078) + val Orange = Color(0xFFF07800) + val Yellow = Color(0xFFF0D800) + const val MaxYellowStop = 0.16f + const val MaxOrangeStop = 0.33f + const val MaxPinkStop = 0.5f + const val MaxPurpleStop = 0.67f + const val MaxBlueStop = 0.83f + } +} +// [END android_compose_interactions_neon_node] + +// [START android_compose_interactions_neon_indication] +data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory { + + override fun create(interactionSource: InteractionSource): DelegatableNode { + return NeonNode( + shape, + // Double the border size for a stronger press effect + borderWidth * 2, + interactionSource + ) + } +} +// [END android_compose_interactions_neon_indication] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt new file mode 100644 index 000000000..d48412c3c --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt @@ -0,0 +1,499 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.touchinput.focus + +import androidx.compose.foundation.IndicationNodeFactory +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusDirection.Companion.Down +import androidx.compose.ui.focus.FocusDirection.Companion.Right +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.FocusRequester.Companion.Cancel +import androidx.compose.ui.focus.FocusRequester.Companion.Default +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.Blue +import androidx.compose.ui.graphics.Color.Companion.Green +import androidx.compose.ui.graphics.Color.Companion.Red +import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Preview +@Composable +private fun BasicSample() { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + // [START android_compose_touchinput_focus_horizontal] + Column { + Row { + TextButton({ }) { Text("First field") } + TextButton({ }) { Text("Second field") } + } + Row { + TextButton({ }) { Text("Third field") } + TextButton({ }) { Text("Fourth field") } + } + } + // [END android_compose_touchinput_focus_horizontal] + } +} + +@Preview +@Composable +private fun BasicSample2() { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + // [START android_compose_touchinput_focus_vertical] + Row { + Column { + TextButton({ }) { Text("First field") } + TextButton({ }) { Text("Second field") } + } + Column { + TextButton({ }) { Text("Third field") } + TextButton({ }) { Text("Fourth field") } + } + } + // [END android_compose_touchinput_focus_vertical] + } +} + +@Preview +@Composable +fun OverrideDefaultOrder() { + // [START android_compose_touchinput_focus_override_refs] + val (first, second, third, fourth) = remember { FocusRequester.createRefs() } + // [END android_compose_touchinput_focus_override_refs] + // [START android_compose_touchinput_focus_override] + Column { + Row { + TextButton({}, Modifier.focusRequester(first)) { Text("First field") } + TextButton({}, Modifier.focusRequester(third)) { Text("Third field") } + } + + Row { + TextButton({}, Modifier.focusRequester(second)) { Text("Second field") } + TextButton({}, Modifier.focusRequester(fourth)) { Text("Fourth field") } + } + } + // [END android_compose_touchinput_focus_override] + + // [START android_compose_touchinput_focus_override_use] + Column { + Row { + TextButton( + {}, + Modifier + .focusRequester(first) + .focusProperties { next = second } + ) { + Text("First field") + } + TextButton( + {}, + Modifier + .focusRequester(third) + .focusProperties { next = fourth } + ) { + Text("Third field") + } + } + + Row { + TextButton( + {}, + Modifier + .focusRequester(second) + .focusProperties { next = third } + ) { + Text("Second field") + } + TextButton( + {}, + Modifier + .focusRequester(fourth) + .focusProperties { next = first } + ) { + Text("Fourth field") + } + } + } + // [END android_compose_touchinput_focus_override_use] +} + +@Preview +@Composable +fun OverrideTwoDimensionalOrder() { + + val (second, third, fourth) = remember { FocusRequester.createRefs() } + + // [START android_compose_touchinput_focus_override_2d] + TextButton( + onClick = {}, + modifier = Modifier + .focusRequester(fourth) + .focusProperties { + down = third + right = second + } + ) {} + // [END android_compose_touchinput_focus_override_2d] +} + +@Composable +private fun FocusGroup() { + + @Composable + fun FilterChipA() { + } + + @Composable + fun FilterChipB() { + } + + @Composable + fun FilterChipC() { + } + + @Composable + fun SweetsCard(sweets: Int) { + } + + val chocolates = 0 + + // [START android_compose_touchinput_focus_group] + LazyVerticalGrid(columns = GridCells.Fixed(4)) { + item(span = { GridItemSpan(maxLineSpan) }) { + Row(modifier = Modifier.focusGroup()) { + FilterChipA() + FilterChipB() + FilterChipC() + } + } + items(chocolates) { + SweetsCard(sweets = it) + } + } + // [END android_compose_touchinput_focus_group] +} + +@Composable +private fun Focusable() { + // [START android_compose_touchinput_focus_focusable] + var color by remember { mutableStateOf(Green) } + Box( + Modifier + .background(color) + .onFocusChanged { color = if (it.isFocused) Blue else Green } + .focusable() + ) { + Text("Focusable 1") + } + // [END android_compose_touchinput_focus_focusable] +} + +@Composable +private fun Unfocusable() { + // [START android_compose_touchinput_focus_unfocusable] + var checked by remember { mutableStateOf(false) } + + Switch( + checked = checked, + onCheckedChange = { checked = it }, + // Prevent component from being focused + modifier = Modifier + .focusProperties { canFocus = false } + ) + // [END android_compose_touchinput_focus_unfocusable] +} + +@Composable +private fun RequestFocus() { + // [START android_compose_touchinput_focus_request] + val focusRequester = remember { FocusRequester() } + var text by remember { mutableStateOf("") } + + TextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier.focusRequester(focusRequester) + ) + // [END android_compose_touchinput_focus_request] +} + +@Composable +private fun RequestFocus2() { + // [START android_compose_touchinput_focus_request2] + val focusRequester = remember { FocusRequester() } + var text by remember { mutableStateOf("") } + + TextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier.focusRequester(focusRequester) + ) + + Button(onClick = { focusRequester.requestFocus() }) { + Text("Request focus on TextField") + } + // [END android_compose_touchinput_focus_request2] +} + +@Composable +private fun Capture() { + var text by remember { mutableStateOf("") } + // [START android_compose_touchinput_focus_capture] + val textField = remember { FocusRequester() } + + TextField( + value = text, + onValueChange = { + text = it + + if (it.length > 3) { + textField.captureFocus() + } else { + textField.freeFocus() + } + }, + modifier = Modifier.focusRequester(textField) + ) + // [END android_compose_touchinput_focus_capture] +} + +@Composable +private fun ModifierOrder() { + + val (item1, item2) = remember { FocusRequester.createRefs() } + + // [START android_compose_touchinput_focus_order_1] + Modifier + .focusProperties { right = item1 } + .focusProperties { right = item2 } + .focusable() + // [END android_compose_touchinput_focus_order_1] + // [START android_compose_touchinput_focus_order_2] + Modifier + .focusProperties { right = Default } + .focusProperties { right = item1 } + .focusProperties { right = item2 } + .focusable() + // [END android_compose_touchinput_focus_order_2] +} + +// [START android_compose_touchinput_focus_order_overwrite] +@Composable +fun FancyButton(modifier: Modifier = Modifier) { + Row(modifier.focusProperties { canFocus = false }) { + Text("Click me") + Button(onClick = { }) { Text("OK") } + } +} +// [END android_compose_touchinput_focus_order_overwrite] + +@Composable +private fun CallFancyButton() { +// [START android_compose_touchinput_focus_order_overwrite_call] + FancyButton(Modifier.focusProperties { canFocus = true }) +// [END android_compose_touchinput_focus_order_overwrite_call] +} + +@Composable +private fun ModifierOrder2() { + // [START android_compose_touchinput_focus_order_request_1] + Box( + Modifier + .focusable() + .focusRequester(Default) + .onFocusChanged {} + ) + // [END android_compose_touchinput_focus_order_request_1] + // [START android_compose_touchinput_focus_order_request_2] + Box( + Modifier + .onFocusChanged {} + .focusRequester(Default) + .focusable() + ) + // [END android_compose_touchinput_focus_order_request_2] + // [START android_compose_touchinput_focus_order_request_3] + Box( + Modifier + .focusRequester(Default) + .onFocusChanged {} + .focusable() + ) + // [END android_compose_touchinput_focus_order_request_3] +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun RedirectFocus() { + // [START android_compose_touchinput_focus_redirect] + val otherComposable = remember { FocusRequester() } + + Modifier.focusProperties { + exit = { focusDirection -> + when (focusDirection) { + Right -> Cancel + Down -> otherComposable + else -> Default + } + } + } + // [END android_compose_touchinput_focus_redirect] +} + +@Composable +private fun FocusAdvancing() { + // [START android_compose_touchinput_focus_advancing] + val focusManager = LocalFocusManager.current + var text by remember { mutableStateOf("") } + + TextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier.onPreviewKeyEvent { + when { + KeyEventType.KeyUp == it.type && Key.Tab == it.key -> { + focusManager.moveFocus(FocusDirection.Next) + true + } + + else -> false + } + } + ) + // [END android_compose_touchinput_focus_advancing] +} + +@Composable +private fun ReactToFocus() { + // [START android_compose_touchinput_focus_react] + var color by remember { mutableStateOf(Color.White) } + Card( + modifier = Modifier + .onFocusChanged { + color = if (it.isFocused) Red else White + } + .border(5.dp, color) + ) {} + // [END android_compose_touchinput_focus_react] +} + +// [START android_compose_touchinput_focus_advanced_cues] +private class MyHighlightIndicationNode(private val interactionSource: InteractionSource) : + Modifier.Node(), DrawModifierNode { + private var isFocused = false + + override fun onAttach() { + coroutineScope.launch { + var focusCount = 0 + interactionSource.interactions.collect { interaction -> + when (interaction) { + is FocusInteraction.Focus -> focusCount++ + is FocusInteraction.Unfocus -> focusCount-- + } + val focused = focusCount > 0 + if (isFocused != focused) { + isFocused = focused + invalidateDraw() + } + } + } + } + + override fun ContentDrawScope.draw() { + drawContent() + if (isFocused) { + drawRect(size = size, color = Color.White, alpha = 0.2f) + } + } +} + +// [END android_compose_touchinput_focus_advanced_cues] + +// [START android_compose_touchinput_focus_indication] +object MyHighlightIndication : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return MyHighlightIndicationNode(interactionSource) + } + + override fun hashCode(): Int = -1 + + override fun equals(other: Any?) = other === this +} +// [END android_compose_touchinput_focus_indication] + +@Composable +private fun ApplyIndication() { + // [START android_compose_touchinput_focus_apply_indication] + var interactionSource = remember { MutableInteractionSource() } + + Card( + modifier = Modifier + .clickable( + interactionSource = interactionSource, + indication = MyHighlightIndication, + enabled = true, + onClick = { } + ) + ) { + Text("hello") + } + // [END android_compose_touchinput_focus_apply_indication] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt new file mode 100644 index 000000000..9fcedffe2 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt @@ -0,0 +1,395 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.touchinput.gestures + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.rememberTransformableState +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.gestures.transformable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FractionalThreshold +import androidx.compose.material.rememberSwipeableState +import androidx.compose.material.swipeable +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +@Preview +// [START android_compose_touchinput_gestures_clickable] +@Composable +private fun ClickableSample() { + val count = remember { mutableIntStateOf(0) } + // content that you want to make clickable + Text( + text = count.value.toString(), + modifier = Modifier.clickable { count.value += 1 } + ) +} +// [END android_compose_touchinput_gestures_clickable] + +@Preview +@Composable +private fun WithPointerInput() { + val count = remember { mutableIntStateOf(0) } + // content that you want to make clickable + Text( + text = count.value.toString(), + modifier = + // [START android_compose_touchinput_gestures_pointerinput] + Modifier.pointerInput(Unit) { + detectTapGestures( + onPress = { /* Called when the gesture starts */ }, + onDoubleTap = { /* Called on Double Tap */ }, + onLongPress = { /* Called on Long Press */ }, + onTap = { /* Called on Tap */ } + ) + } + // [END android_compose_touchinput_gestures_pointerinput] + ) +} + +@Preview +// [START android_compose_touchinput_gestures_vertical_scroll] +@Composable +private fun ScrollBoxes() { + Column( + modifier = Modifier + .background(Color.LightGray) + .size(100.dp) + .verticalScroll(rememberScrollState()) + ) { + repeat(10) { + Text("Item $it", modifier = Modifier.padding(2.dp)) + } + } +} +// [END android_compose_touchinput_gestures_vertical_scroll] + +@Preview +// [START android_compose_touchinput_gestures_smooth_scroll] +@Composable +private fun ScrollBoxesSmooth() { + // Smoothly scroll 100px on first composition + val state = rememberScrollState() + LaunchedEffect(Unit) { state.animateScrollTo(100) } + + Column( + modifier = Modifier + .background(Color.LightGray) + .size(100.dp) + .padding(horizontal = 8.dp) + .verticalScroll(state) + ) { + repeat(10) { + Text("Item $it", modifier = Modifier.padding(2.dp)) + } + } +} +// [END android_compose_touchinput_gestures_smooth_scroll] + +@Preview +// [START android_compose_touchinput_gestures_scrollable] +@Composable +private fun ScrollableSample() { + // actual composable state + var offset by remember { mutableFloatStateOf(0f) } + Box( + Modifier + .size(150.dp) + .scrollable( + orientation = Orientation.Vertical, + // Scrollable state: describes how to consume + // scrolling delta and update offset + state = rememberScrollableState { delta -> + offset += delta + delta + } + ) + .background(Color.LightGray), + contentAlignment = Alignment.Center + ) { + Text(offset.toString()) + } +} +// [END android_compose_touchinput_gestures_scrollable] + +// [START android_compose_touchinput_gestures_nested_scroll] +@Composable +private fun AutomaticNestedScroll() { + val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White) + Box( + modifier = Modifier + .background(Color.LightGray) + .verticalScroll(rememberScrollState()) + .padding(32.dp) + ) { + Column { + repeat(6) { + Box( + modifier = Modifier + .height(128.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + "Scroll here", + modifier = Modifier + .border(12.dp, Color.DarkGray) + .background(brush = gradient) + .padding(24.dp) + .height(150.dp) + ) + } + } + } + } +} +// [END android_compose_touchinput_gestures_nested_scroll] + +private object NestedScrollInterop { + // [START android_compose_touchinput_gestures_nested_scroll_interop_activity] + open class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + findViewById(R.id.compose_view).apply { + setContent { + val nestedScrollInterop = rememberNestedScrollInteropConnection() + // Add the nested scroll connection to your top level @Composable element + // using the nestedScroll modifier. + LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) { + items(20) { item -> + Box( + modifier = Modifier + .padding(16.dp) + .height(56.dp) + .fillMaxWidth() + .background(Color.Gray), + contentAlignment = Alignment.Center + ) { + Text(item.toString()) + } + } + } + } + } + } + } + // [END android_compose_touchinput_gestures_nested_scroll_interop_activity] + + object R { + object id { + val compose_view = 1 + } + + object layout { + val activity_main = 0 + } + } +} + +// [START android_compose_touchinput_gestures_draggable] +@Composable +private fun DraggableText() { + var offsetX by remember { mutableFloatStateOf(0f) } + Text( + modifier = Modifier + .offset { IntOffset(offsetX.roundToInt(), 0) } + .draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState { delta -> + offsetX += delta + } + ), + text = "Drag me!" + ) +} +// [END android_compose_touchinput_gestures_draggable] + +// [START android_compose_touchinput_gestures_draggable_pointerinput] +@Composable +private fun DraggableTextLowLevel() { + Box(modifier = Modifier.fillMaxSize()) { + var offsetX by remember { mutableFloatStateOf(0f) } + var offsetY by remember { mutableFloatStateOf(0f) } + + Box( + Modifier + .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } + .background(Color.Blue) + .size(50.dp) + .pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + offsetX += dragAmount.x + offsetY += dragAmount.y + } + } + ) + } +} +// [END android_compose_touchinput_gestures_draggable_pointerinput] + +// [START android_compose_touchinput_gestures_swipeable] +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun SwipeableSample() { + val width = 96.dp + val squareSize = 48.dp + + val swipeableState = rememberSwipeableState(0) + val sizePx = with(LocalDensity.current) { squareSize.toPx() } + val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states + + Box( + modifier = Modifier + .width(width) + .swipeable( + state = swipeableState, + anchors = anchors, + thresholds = { _, _ -> FractionalThreshold(0.3f) }, + orientation = Orientation.Horizontal + ) + .background(Color.LightGray) + ) { + Box( + Modifier + .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } + .size(squareSize) + .background(Color.DarkGray) + ) + } +} +// [END android_compose_touchinput_gestures_swipeable] + +// [START android_compose_touchinput_gestures_transformable] +@Composable +private fun TransformableSample() { + // set up all transformation states + var scale by remember { mutableFloatStateOf(1f) } + var rotation by remember { mutableFloatStateOf(0f) } + var offset by remember { mutableStateOf(Offset.Zero) } + val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> + scale *= zoomChange + rotation += rotationChange + offset += offsetChange + } + Box( + Modifier + // apply other transformations like rotation and zoom + // on the pizza slice emoji + .graphicsLayer( + scaleX = scale, + scaleY = scale, + rotationZ = rotation, + translationX = offset.x, + translationY = offset.y + ) + // add transformable to listen to multitouch transformation events + // after offset + .transformable(state = state) + .background(Color.Blue) + .fillMaxSize() + ) +} +// [END android_compose_touchinput_gestures_transformable] + +@Composable +fun NestedScrollSample() { + + // [START android_compose_touchinput_gestures_nestedscrollconnection] + val nestedScrollConnection = object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + println("Received onPreScroll callback.") + return Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + println("Received onPostScroll callback.") + return Offset.Zero + } + } + // [END android_compose_touchinput_gestures_nestedscrollconnection] + + // [START android_compose_touchinput_gestures_nestedscrolldisabled] + val disabledNestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + return if (source == NestedScrollSource.SideEffect) { + available + } else { + Offset.Zero + } + } + } + } + // [END android_compose_touchinput_gestures_nestedscrolldisabled] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt new file mode 100644 index 000000000..610143cec --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.touchinput.keyboardinput + +import android.os.Build +import android.os.Bundle +import android.view.KeyEvent +import android.view.KeyboardShortcutGroup +import android.view.KeyboardShortcutInfo +import android.view.Menu +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text + +class MainActivity : ComponentActivity() { + // Activity codes such as overridden onStart method. + + @RequiresApi(Build.VERSION_CODES.N) + // [START android_compose_keyboard_shortcuts_helper] + override fun onProvideKeyboardShortcuts( + data: MutableList?, + menu: Menu?, + deviceId: Int + ) { + val shortcutGroup = + KeyboardShortcutGroup( + "Cursor movement", + listOf( + KeyboardShortcutInfo("Up", KeyEvent.KEYCODE_P, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Down", KeyEvent.KEYCODE_N, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Forward", KeyEvent.KEYCODE_F, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Backward", KeyEvent.KEYCODE_B, KeyEvent.META_CTRL_ON), + ) + ) + data?.add(shortcutGroup) + } + // [END android_compose_keyboard_shortcuts_helper] +} + +class AnotherActivity : ComponentActivity() { + + @RequiresApi(Build.VERSION_CODES.N) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MaterialTheme { + // [START android_compose_keyboard_shortcuts_helper_request] + val activity = LocalActivity.current + + Button( + onClick = { + activity?.requestShowKeyboardShortcuts() + } + ) { + Text(text = "Show keyboard shortcuts") + } + // [END android_compose_keyboard_shortcuts_helper_request] + } + } + } + + @RequiresApi(Build.VERSION_CODES.N) + // [START android_compose_keyboard_shortcuts_helper_with_groups] + override fun onProvideKeyboardShortcuts( + data: MutableList?, + menu: Menu?, + deviceId: Int + ) { + val cursorMovement = KeyboardShortcutGroup( + "Cursor movement", + listOf( + KeyboardShortcutInfo("Up", KeyEvent.KEYCODE_P, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Down", KeyEvent.KEYCODE_N, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Forward", KeyEvent.KEYCODE_F, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Backward", KeyEvent.KEYCODE_B, KeyEvent.META_CTRL_ON), + ) + ) + + val messageEdit = KeyboardShortcutGroup( + "Message editing", + listOf( + KeyboardShortcutInfo("Select All", KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo( + "Send a message", + KeyEvent.KEYCODE_ENTER, + KeyEvent.META_SHIFT_ON + ) + ) + ) + + data?.add(cursorMovement) + data?.add(messageEdit) + } + // [END android_compose_keyboard_shortcuts_helper_with_groups] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt new file mode 100644 index 000000000..d0c08c544 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt @@ -0,0 +1,337 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.touchinput.keyboardinput + +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.onFocusEvent +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isAltPressed +import androidx.compose.ui.input.key.isCtrlPressed +import androidx.compose.ui.input.key.isMetaPressed +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp + +@Suppress("unused") +@Composable +fun CommandsScreen() { + val context = LocalContext.current + var playerState by rememberSaveable { mutableStateOf(false) } + + val doSomething = { + showToast(context, "Doing something") + } + + val doAnotherThing = { + showToast(context, "Doing another thing") + } + + val togglePlayPause = { + playerState = !playerState + val message = if (playerState) { + "Playing" + } else { + "Paused" + } + showToast(context, message) + } + + val actionC = { + showToast(context, "Action C") + } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(8.dp) + ) { + KeyEvents(doSomething) + ModifierKeys(doSomething) + SpacebarAndEnterKeyTriggersClickEvents(togglePlayPause) + UnconsumedKeyEvents(doSomething, doAnotherThing, actionC) + PreviewKeyEvents() + InterceptKeyEvents( + doSomething, + { keyEvent -> + showToast(context, "onPreviewKeyEvent: ${keyEvent.key.keyCode}") + }, + { keyEvent -> + showToast(context, "onKeyEvent: ${keyEvent.key.keyCode}") + } + ) + } +} + +fun showToast(context: Context, message: String) { + val toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) + toast.show() +} + +@Composable +private fun BoxWithFocusIndication( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + var isFocused by remember { mutableStateOf(false) } + val backgroundColor = if (isFocused) { + MaterialTheme.colorScheme.surfaceVariant + } else { + MaterialTheme.colorScheme.surface + } + Box( + modifier = modifier + .onFocusEvent { + isFocused = it.isFocused + } + .background(backgroundColor), + content = content + ) +} + +@Composable +private fun KeyEvents( + doSomething: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier) { + // [START android_compose_touchinput_keyboardinput_keyevents] + Box( + modifier = Modifier + .onKeyEvent { + if ( + it.type == KeyEventType.KeyUp && + it.key == Key.S + ) { + doSomething() + true + } else { + false + } + } + .focusable() + ) { + Text("Press S key") + } + // [END android_compose_touchinput_keyboardinput_keyevents] + } +} + +@Composable +private fun ModifierKeys( + doSomething: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_modifierkeys] + Box( + modifier = Modifier + .focusable() + .onKeyEvent { + if ( + it.type == KeyEventType.KeyUp && + it.key == Key.S && + !it.isAltPressed && + !it.isCtrlPressed && + !it.isMetaPressed && + !it.isShiftPressed + ) { + doSomething() + true + } else { + false + } + } + ) { + Text("Press S key with a modifier key") + } + // [END android_compose_touchinput_keyboardinput_modifierkeys] + } +} + +@Composable +private fun SpacebarAndEnterKeyTriggersClickEvents( + togglePausePlay: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_spacebar] + MediaPlayer(modifier = Modifier.clickable { togglePausePlay() }) + // [END android_compose_touchinput_keyboardinput_spacebar] + } +} + +@Composable +private fun MediaPlayer( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(200.dp) + .background(MaterialTheme.colorScheme.primaryContainer) + ) +} + +@Composable +private fun UnconsumedKeyEvents( + actionA: () -> Unit, + actionB: () -> Unit, + actionC: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_unconsumedkeyevents] + OuterComponent( + modifier = Modifier.onKeyEvent { + when { + it.type == KeyEventType.KeyUp && it.key == Key.S -> { + actionB() // This function is never called. + true + } + + it.type == KeyEventType.KeyUp && it.key == Key.D -> { + actionC() + true + } + + else -> false + } + } + ) { + InnerComponent( + modifier = Modifier.onKeyEvent { + if (it.type == KeyEventType.KeyUp && it.key == Key.S) { + actionA() + true + } else { + false + } + } + ) + } + // [END android_compose_touchinput_keyboardinput_unconsumedkeyevents] + } +} + +@Composable +private fun OuterComponent( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) = + Box(content = content, modifier = modifier.focusable()) + +@Composable +private fun InnerComponent( + modifier: Modifier = Modifier +) { + Card(modifier = modifier.focusable()) { + Text("Press S key or D key", modifier = Modifier.padding(16.dp)) + } +} + +@Composable +private fun PreviewKeyEvents() { + // [START android_compose_touchinput_keyboardinput_previewkeyevents] + val focusManager = LocalFocusManager.current + var textFieldValue by remember { mutableStateOf(TextFieldValue()) } + + TextField( + textFieldValue, + onValueChange = { + textFieldValue = it + }, + modifier = Modifier.onPreviewKeyEvent { + if (it.type == KeyEventType.KeyUp && it.key == Key.Tab) { + focusManager.moveFocus(FocusDirection.Next) + true + } else { + false + } + } + ) + // [END android_compose_touchinput_keyboardinput_previewkeyevents] +} + +@Composable +private fun InterceptKeyEvents( + previewSKey: () -> Unit, + actionForPreview: (KeyEvent) -> Unit, + actionForKeyEvent: (KeyEvent) -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_interceptevents] + Column( + modifier = Modifier.onPreviewKeyEvent { + if (it.key == Key.S) { + previewSKey() + true + } else { + false + } + } + ) { + Box( + modifier = Modifier + .focusable() + .onPreviewKeyEvent { + actionForPreview(it) + false + } + .onKeyEvent { + actionForKeyEvent(it) + true + } + ) { + Text("Press any key") + } + } + // [END android_compose_touchinput_keyboardinput_interceptevents] + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/TapAndPress.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/TapAndPress.kt new file mode 100644 index 000000000..dbe22282b --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/TapAndPress.kt @@ -0,0 +1,387 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.touchinput.pointerinput + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.DeleteOutline +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material.icons.filled.RadioButtonUnchecked +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import com.example.compose.snippets.R +import com.example.compose.snippets.ui.theme.SnippetsTheme +import com.example.compose.snippets.util.rememberRandomSampleImageUrl + +private class Photo( + val id: Int, + val url: String, + val highResUrl: String +) + +@Preview +@Composable +private fun MyApp() { + val photos = List(100) { + val url = rememberRandomSampleImageUrl(width = 256) + Photo(it, url, url.replace("256", "1024")) + } + SnippetsTheme { + // Uncomment the sample you want to run + ImageGrid(photos) +// ImageGridContextMenu(photos) +// MultiselectMode(photos) + } +} + +@Composable +private fun ClickableSurfaceSample() { + SnippetsTheme { + // [START android_compose_touchinput_pointerinput_onclick] + Surface(onClick = { /* handle click */ }) { + Text("Click me!", Modifier.padding(24.dp)) + } + // [END android_compose_touchinput_pointerinput_onclick] + } +} + +// [START android_compose_touchinput_pointerinput_clickable] +@Composable +private fun ImageGrid(photos: List) { + var activePhotoId by rememberSaveable { mutableStateOf(null) } + LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) { + items(photos, { it.id }) { photo -> + ImageItem( + photo, + // [START android_compose_touchinput_pointerinput_clickable_highlight] + Modifier.clickable { activePhotoId = photo.id } + // [END android_compose_touchinput_pointerinput_clickable_highlight] + ) + } + } + if (activePhotoId != null) { + FullScreenImage( + photo = photos.first { it.id == activePhotoId }, + onDismiss = { activePhotoId = null } + ) + } +} +// [END android_compose_touchinput_pointerinput_clickable] + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ImageGridContextMenu(photos: List) { + var activePhotoId by rememberSaveable { mutableStateOf(null) } + // [START android_compose_touchinput_pointerinput_long_clickable] + var contextMenuPhotoId by rememberSaveable { mutableStateOf(null) } + val haptics = LocalHapticFeedback.current + LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) { + items(photos, { it.id }) { photo -> + ImageItem( + photo, + // [START android_compose_touchinput_pointerinput_long_clickable_highlight] + Modifier + .combinedClickable( + onClick = { activePhotoId = photo.id }, + onLongClick = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + contextMenuPhotoId = photo.id + }, + onLongClickLabel = stringResource(R.string.open_context_menu) + ) + // [END android_compose_touchinput_pointerinput_long_clickable_highlight] + ) + } + } + if (contextMenuPhotoId != null) { + PhotoActionsSheet( + photo = photos.first { it.id == contextMenuPhotoId }, + onDismissSheet = { contextMenuPhotoId = null } + ) + } + // [END android_compose_touchinput_pointerinput_long_clickable] + if (activePhotoId != null) { + FullScreenImage( + photo = photos.first { it.id == activePhotoId }, + onDismiss = { activePhotoId = null } + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun MultiselectMode(photos: List) { + // [START android_compose_touchinput_pointerinput_multiselect] + var activePhotoId by rememberSaveable { mutableStateOf(null) } + var selectedIds by rememberSaveable { mutableStateOf(setOf()) } + val inSelectionMode = selectedIds.isNotEmpty() + + LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) { + items(photos, { it.id }) { photo -> + val selected = selectedIds.contains(photo.id) + SelectableImageItem( + photo = photo, + inSelectionMode = inSelectionMode, + selected = selected, + Modifier + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + if (inSelectionMode) { + if (selected) selectedIds -= photo.id else selectedIds += photo.id + } else { + activePhotoId = photo.id + } + }, + onLongClick = { selectedIds += photo.id }, + ) + ) + } + } + if (inSelectionMode) { + ElevatedButton( + onClick = { selectedIds = emptySet() }, + contentPadding = PaddingValues(8.dp), + modifier = Modifier.padding(8.dp) + ) { + Icon( + Icons.Default.Close, null + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(selectedIds.count().toString()) + } + } + // [END android_compose_touchinput_pointerinput_multiselect] + if (activePhotoId != null) { + FullScreenImage( + photo = photos.first { it.id == activePhotoId }, + onDismiss = { activePhotoId = null } + ) + } +} + +@Composable +private fun ImageItem(photo: Photo, modifier: Modifier = Modifier) { + Image( + painter = rememberAsyncImagePainter(model = photo.url), + contentDescription = null, + modifier = modifier.aspectRatio(1f) + ) +} + +@Composable +private fun SelectableImageItem( + photo: Photo, + inSelectionMode: Boolean, + selected: Boolean, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .aspectRatio(1f) + .background(MaterialTheme.colorScheme.tertiaryContainer) + ) { + Image( + painter = rememberAsyncImagePainter(model = photo.url), + contentDescription = null, + modifier = Modifier + .matchParentSize() + .then( + if (selected) Modifier + .padding(16.dp) + .clip(RoundedCornerShape(12.dp)) + else Modifier + ) + ) + if (inSelectionMode) { + val icon = if (selected) Icons.Filled.CheckCircle else Icons.Filled.RadioButtonUnchecked + val tint = + if (selected) MaterialTheme.colorScheme.primary else Color.White.copy(alpha = 0.7f) + + Icon(icon, null, tint = tint, modifier = Modifier.padding(6.dp)) + } + } +} + +@Composable +private fun FullScreenImage( + photo: Photo, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Scrim(onDismiss, Modifier.fillMaxSize()) + ImageWithZoom(photo, Modifier.aspectRatio(1f)) + } +} + +// [START android_compose_touchinput_pointerinput_scrim] +@Composable +private fun Scrim(onClose: () -> Unit, modifier: Modifier = Modifier) { + val strClose = stringResource(R.string.close) + Box( + modifier + // handle pointer input + // [START android_compose_touchinput_pointerinput_scrim_highlight] + .pointerInput(onClose) { detectTapGestures { onClose() } } + // [END android_compose_touchinput_pointerinput_scrim_highlight] + // handle accessibility services + .semantics(mergeDescendants = true) { + contentDescription = strClose + onClick { + onClose() + true + } + } + // handle physical keyboard input + .onKeyEvent { + if (it.key == Key.Escape) { + onClose() + true + } else { + false + } + } + // draw scrim + .background(Color.DarkGray.copy(alpha = 0.75f)) + ) +} +// [END android_compose_touchinput_pointerinput_scrim] + +@Composable +private fun ImageWithZoom(photo: Photo, modifier: Modifier = Modifier) { + // [START android_compose_touchinput_pointerinput_double_tap_zoom] + var zoomed by remember { mutableStateOf(false) } + var zoomOffset by remember { mutableStateOf(Offset.Zero) } + Image( + painter = rememberAsyncImagePainter(model = photo.highResUrl), + contentDescription = null, + modifier = modifier + // [START android_compose_touchinput_pointerinput_double_tap_zoom_highlight] + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { tapOffset -> + zoomOffset = if (zoomed) Offset.Zero else + calculateOffset(tapOffset, size) + zoomed = !zoomed + } + ) + } + // [END android_compose_touchinput_pointerinput_double_tap_zoom_highlight] + .graphicsLayer { + scaleX = if (zoomed) 2f else 1f + scaleY = if (zoomed) 2f else 1f + translationX = zoomOffset.x + translationY = zoomOffset.y + } + ) + // [END android_compose_touchinput_pointerinput_double_tap_zoom] +} + +private fun calculateOffset(tapOffset: Offset, size: IntSize): Offset { + val offsetX = (-(tapOffset.x - (size.width / 2f)) * 2f) + .coerceIn(-size.width / 2f, size.width / 2f) + return Offset(offsetX, 0f) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PhotoActionsSheet( + @Suppress("UNUSED_PARAMETER") photo: Photo, + onDismissSheet: () -> Unit +) { + ModalBottomSheet( + onDismissRequest = onDismissSheet + ) { + ListItem( + headlineContent = { Text("Add to album") }, + leadingContent = { Icon(Icons.Default.Add, null) } + ) + ListItem( + headlineContent = { Text("Add to favorites") }, + leadingContent = { Icon(Icons.Default.FavoriteBorder, null) } + ) + ListItem( + headlineContent = { Text("Share") }, + leadingContent = { Icon(Icons.Default.Share, null) } + ) + ListItem( + headlineContent = { Text("Remove") }, + leadingContent = { Icon(Icons.Default.DeleteOutline, null) } + ) + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/UnderstandGesturesSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/UnderstandGesturesSnippets.kt new file mode 100644 index 000000000..a892e6126 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/pointerinput/UnderstandGesturesSnippets.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("ControlFlowWithEmptyBody", "UNUSED_VARIABLE", "unused") + +package com.example.compose.snippets.touchinput.pointerinput + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +private fun Layering() { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + // [START android_compose_touchinput_pointerinput_layering] + // Talkback: "Click me!, Button, double tap to activate" + Button(onClick = { /* TODO */ }) { Text("Click me!") } + // Talkback: "Click me!, double tap to activate" + Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") } + // [END android_compose_touchinput_pointerinput_layering] + } +} + +@Preview +// [START android_compose_touchinput_pointerinput_raw_event_listening] +@Composable +private fun LogPointerEvents(filter: PointerEventType? = null) { + var log by remember { mutableStateOf("") } + Column { + Text(log) + Box( + Modifier + .size(100.dp) + .background(Color.Red) + .pointerInput(filter) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + // handle pointer event + if (filter == null || event.type == filter) { + log = "${event.type}, ${event.changes.first().position}" + } + } + } + } + ) + } +} +// [END android_compose_touchinput_pointerinput_raw_event_listening] + +@Composable +private fun Consuming() { + Box( + // [START android_compose_touchinput_pointerinput_consuming] + Modifier.pointerInput(Unit) { + + awaitEachGesture { + while (true) { + val event = awaitPointerEvent() + // consume all changes + event.changes.forEach { it.consume() } + } + } + } + // [END android_compose_touchinput_pointerinput_consuming] + ) +} + +@Composable +private fun CheckConsumption() { + Box( + // [START android_compose_touchinput_pointerinput_check_consumption] + Modifier.pointerInput(Unit) { + awaitEachGesture { + while (true) { + val event = awaitPointerEvent() + if (event.changes.any { it.isConsumed }) { + // A pointer is consumed by another gesture handler + } else { + // Handle unconsumed event + } + } + } + } + // [END android_compose_touchinput_pointerinput_check_consumption] + ) +} + +@Preview +@Composable +private fun IncorrectDoubleGestureDetector() { + // [START android_compose_touchinput_pointerinput_incorrect_double_gesture_detector] + var log by remember { mutableStateOf("") } + Column { + Text(log) + Box( + Modifier + .size(100.dp) + .background(Color.Red) + .pointerInput(Unit) { + detectTapGestures { log = "Tap!" } + // Never reached + detectDragGestures { _, _ -> log = "Dragging" } + } + ) + } + // [END android_compose_touchinput_pointerinput_incorrect_double_gesture_detector] +} + +@Preview +@Composable +private fun CorrectDoubleGestureDetector() { + // [START android_compose_touchinput_pointerinput_correct_double_gesture_detector] + var log by remember { mutableStateOf("") } + Column { + Text(log) + Box( + Modifier + .size(100.dp) + .background(Color.Red) + .pointerInput(Unit) { + detectTapGestures { log = "Tap!" } + } + .pointerInput(Unit) { + // These drag events will correctly be triggered + detectDragGestures { _, _ -> log = "Dragging" } + } + ) + } + // [END android_compose_touchinput_pointerinput_correct_double_gesture_detector] +} + +// [START android_compose_touchinput_pointerinput_simple_clickable] +@Composable +private fun SimpleClickable(onClick: () -> Unit) { + Box( + Modifier + .size(100.dp) + .pointerInput(onClick) { + awaitEachGesture { + awaitFirstDown().also { it.consume() } + val up = waitForUpOrCancellation() + if (up != null) { + up.consume() + onClick() + } + } + } + ) +} +// [END android_compose_touchinput_pointerinput_simple_clickable] + +@Composable +private fun ThreePasses() { + Box( + // [START android_compose_touchinput_pointerinput_three_passes] + Modifier.pointerInput(Unit) { + awaitPointerEventScope { + val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) + val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default + val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) + } + } + // [END android_compose_touchinput_pointerinput_three_passes] + ) +} + +@Preview +@Composable +fun Demo() { + var log by remember { mutableStateOf("") } + var log2 by remember { mutableStateOf("") } + Column( + Modifier + .background(Color.White) + .size(300.dp) + ) { + Text(log) + Text(log2) + Box { + Box( + Modifier + .size(100.dp) + .background(Color.Red) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + log2 = "RED: ${event.type}, ${event.changes.first().position}" + } + } + } + ) + Box( + Modifier + .size(100.dp) + .offset(x = 50.dp, y = 50.dp) + .background(Color.Blue) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + log = "BLUE: ${event.type}, ${event.changes.first().position}" + } + } + } + ) + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt new file mode 100644 index 000000000..6b9fa3fda --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/userinteractions/UserInteractions.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("DEPRECATION_ERROR") + +package com.example.compose.snippets.touchinput.userinteractions + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring +import androidx.compose.foundation.Indication +import androidx.compose.foundation.IndicationInstance +import androidx.compose.foundation.IndicationNodeFactory +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalRippleConfiguration +import androidx.compose.material.RippleConfiguration +import androidx.compose.material.ripple +import androidx.compose.material.ripple.LocalRippleTheme +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material.ripple.RippleTheme +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode +import com.example.compose.snippets.architecture.Button +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +// [START android_compose_userinteractions_scale_indication] +// [START android_compose_userinteractions_scale_indication_object] +object ScaleIndication : Indication { + @Composable + override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { + // key the remember against interactionSource, so if it changes we create a new instance + val instance = remember(interactionSource) { ScaleIndicationInstance() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collectLatest { interaction -> + when (interaction) { + is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition) + is PressInteraction.Release -> instance.animateToResting() + is PressInteraction.Cancel -> instance.animateToResting() + } + } + } + + return instance + } +} +// [END android_compose_userinteractions_scale_indication_object] + +// [START android_compose_userinteractions_scale_indication_instance] +private class ScaleIndicationInstance : IndicationInstance { + var currentPressPosition: Offset = Offset.Zero + val animatedScalePercent = Animatable(1f) + + suspend fun animateToPressed(pressPosition: Offset) { + currentPressPosition = pressPosition + animatedScalePercent.animateTo(0.9f, spring()) + } + + suspend fun animateToResting() { + animatedScalePercent.animateTo(1f, spring()) + } + + override fun ContentDrawScope.drawIndication() { + scale( + scale = animatedScalePercent.value, + pivot = currentPressPosition + ) { + this@drawIndication.drawContent() + } + } +} +// [END android_compose_userinteractions_scale_indication_instance] +// [END android_compose_userinteractions_scale_indication] + +@Composable +private fun RememberRippleExample() { + // [START android_compose_userinteractions_material_remember_ripple] + Box( + Modifier.clickable( + onClick = {}, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple() + ) + ) { + // ... + } + // [END android_compose_userinteractions_material_remember_ripple] +} + +// [START android_compose_userinteractions_material_ripple] +@Composable +private fun RippleExample() { + Box( + Modifier.clickable( + onClick = {}, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple() + ) + ) { + // ... + } +} +// [END android_compose_userinteractions_material_ripple] + +// [START android_compose_userinteractions_disabled_ripple_theme] +private object DisabledRippleTheme : RippleTheme { + + @Composable + override fun defaultColor(): Color = Color.Transparent + + @Composable + override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f) +} + +// [START_EXCLUDE] +@Composable +private fun MyComposable() { +// [END_EXCLUDE] + CompositionLocalProvider(LocalRippleTheme provides DisabledRippleTheme) { + Button { + // ... + } + } +// [START_EXCLUDE silent] +} +// [END_EXCLUDE] +// [END android_compose_userinteractions_disabled_ripple_theme] + +private val MyRippleAlpha = RippleAlpha(0.5f, 0.5f, 0.5f, 0.5f) + +// [START android_compose_userinteractions_disabled_ripple_theme_color_alpha] +private object DisabledRippleThemeColorAndAlpha : RippleTheme { + + @Composable + override fun defaultColor(): Color = Color.Red + + @Composable + override fun rippleAlpha(): RippleAlpha = MyRippleAlpha +} + +// [START_EXCLUDE] +@Composable +private fun MyComposable2() { +// [END_EXCLUDE] + CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) { + Button { + // ... + } + } +// [START_EXCLUDE silent] +} +// [END_EXCLUDE] +// [END android_compose_userinteractions_disabled_ripple_theme_color_alpha] + +// Snippets for new ripple API + +// [START android_compose_userinteractions_scale_indication_node_factory] +object ScaleIndicationNodeFactory : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return ScaleIndicationNode(interactionSource) + } + + override fun hashCode(): Int = -1 + + override fun equals(other: Any?) = other === this +} +// [END android_compose_userinteractions_scale_indication_node_factory] + +// [START android_compose_userinteractions_scale_indication_node] +private class ScaleIndicationNode( + private val interactionSource: InteractionSource +) : Modifier.Node(), DrawModifierNode { + var currentPressPosition: Offset = Offset.Zero + val animatedScalePercent = Animatable(1f) + + private suspend fun animateToPressed(pressPosition: Offset) { + currentPressPosition = pressPosition + animatedScalePercent.animateTo(0.9f, spring()) + } + + private suspend fun animateToResting() { + animatedScalePercent.animateTo(1f, spring()) + } + + override fun onAttach() { + coroutineScope.launch { + interactionSource.interactions.collectLatest { interaction -> + when (interaction) { + is PressInteraction.Press -> animateToPressed(interaction.pressPosition) + is PressInteraction.Release -> animateToResting() + is PressInteraction.Cancel -> animateToResting() + } + } + } + } + + override fun ContentDrawScope.draw() { + scale( + scale = animatedScalePercent.value, + pivot = currentPressPosition + ) { + this@draw.drawContent() + } + } +} +// [END android_compose_userinteractions_scale_indication_node] + +@Composable +fun App() { +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun MyComposableDisabledRippleConfig() { + // [START android_compose_userinteractions_disabled_ripple_configuration] + CompositionLocalProvider(LocalRippleConfiguration provides null) { + Button { + // ... + } + } + // [END android_compose_userinteractions_disabled_ripple_configuration] +} + +// [START android_compose_userinteractions_my_ripple_configuration] +@OptIn(ExperimentalMaterialApi::class) +private val MyRippleConfiguration = + RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha) + +// [START_EXCLUDE] +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun MyComposableMyRippleConfig() { +// [END_EXCLUDE] + CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) { + Button { + // ... + } + } +// [START_EXCLUDE silent] +} +// [END_EXCLUDE] +// [END android_compose_userinteractions_my_ripple_configuration] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt new file mode 100644 index 000000000..6c6006ead --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Color.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +val LavenderDark = Color(0xff23009e) +val LavenderLight = Color(0xFFDDBEFC) + +val RoseDark = Color(0xffaf0060) +val RoseLight = Color(0xFFFFAFC9) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Theme.kt b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Theme.kt new file mode 100644 index 000000000..bce4bd085 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Theme.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.ViewCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun SnippetsTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() + ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Type.kt b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Type.kt new file mode 100644 index 000000000..ab604e852 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/ui/theme/Type.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/util/ImageLoadingSampleUtils.kt b/compose/snippets/src/main/java/com/example/compose/snippets/util/ImageLoadingSampleUtils.kt new file mode 100644 index 000000000..9d0c9b5fb --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/util/ImageLoadingSampleUtils.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.util + +/* +* Copyright 2023 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember + +private val rangeForRandom = (0..100000) + +fun randomSampleImageUrl( + seed: Int = rangeForRandom.random(), + width: Int = 300, + height: Int = width, +): String { + return "https://picsum.photos/seed/$seed/$width/$height" +} + +/** + * Remember a URL generate by [randomSampleImageUrl]. + */ +@Composable +fun rememberRandomSampleImageUrl( + seed: Int = rangeForRandom.random(), + width: Int = 300, + height: Int = width, +): String = remember { randomSampleImageUrl(seed, width, height) } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/util/MaterialColors.kt b/compose/snippets/src/main/java/com/example/compose/snippets/util/MaterialColors.kt new file mode 100644 index 000000000..f1bf85723 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/util/MaterialColors.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package com.example.compose.snippets.util + +import androidx.compose.ui.graphics.Color +object MaterialColors { + val Red50 = Color(0xFFFFEBEE) + val Red100 = Color(0xFFFFCDD2) + val Red200 = Color(0xFFEF9A9A) + val Red300 = Color(0xFFE57373) + val Red400 = Color(0xFFEF5350) + val Red500 = Color(0xFFF44336) + val Red600 = Color(0xFFE53935) + val Red700 = Color(0xFFD32F2F) + val Red800 = Color(0xFFC62828) + val Red900 = Color(0xFFB71C1C) + + val Pink50 = Color(0xFFFCE4EC) + val Pink100 = Color(0xFFF8BBD0) + val Pink200 = Color(0xFFF48FB1) + val Pink300 = Color(0xFFF06292) + val Pink400 = Color(0xFFEC407A) + val Pink500 = Color(0xFFE91E63) + val Pink600 = Color(0xFFD81B60) + val Pink700 = Color(0xFFC2185B) + val Pink800 = Color(0xFFAD1457) + val Pink900 = Color(0xFF880E4F) + + val Purple50 = Color(0xFFF3E5F5) + val Purple100 = Color(0xFFE1BEE7) + val Purple200 = Color(0xFFCE93D8) + val Purple300 = Color(0xFFBA68C8) + val Purple400 = Color(0xFFAB47BC) + val Purple500 = Color(0xFF9C27B0) + val Purple600 = Color(0xFF8E24AA) + val Purple700 = Color(0xFF7B1FA2) + val Purple800 = Color(0xFF6A1B9A) + val Purple900 = Color(0xFF4A148C) + + val DeepPurple50 = Color(0xFFEDE7F6) + val DeepPurple100 = Color(0xFFD1C4E9) + val DeepPurple200 = Color(0xFFB39DDB) + val DeepPurple300 = Color(0xFF9575CD) + val DeepPurple400 = Color(0xFF7E57C2) + val DeepPurple500 = Color(0xFF673AB7) + val DeepPurple600 = Color(0xFF5E35B1) + val DeepPurple700 = Color(0xFF512DA8) + val DeepPurple800 = Color(0xFF4527A0) + val DeepPurple900 = Color(0xFF311B92) + + val Indigo50 = Color(0xFFE8EAF6) + val Indigo100 = Color(0xFFC5CAE9) + val Indigo200 = Color(0xFF9FA8DA) + val Indigo300 = Color(0xFF7986CB) + val Indigo400 = Color(0xFF5C6BC0) + val Indigo500 = Color(0xFF3F51B5) + val Indigo600 = Color(0xFF3949AB) + val Indigo700 = Color(0xFF303F9F) + val Indigo800 = Color(0xFF283593) + val Indigo900 = Color(0xFF1A237E) + + val Blue50 = Color(0xFFE3F2FD) + val Blue100 = Color(0xFFBBDEFB) + val Blue200 = Color(0xFF90CAF9) + val Blue300 = Color(0xFF64B5F6) + val Blue400 = Color(0xFF42A5F5) + val Blue500 = Color(0xFF2196F3) + val Blue600 = Color(0xFF1E88E5) + val Blue700 = Color(0xFF1976D2) + val Blue800 = Color(0xFF1565C0) + val Blue900 = Color(0xFF0D47A1) + + val LightBlue50 = Color(0xFFE1F5FE) + val LightBlue100 = Color(0xFFB3E5FC) + val LightBlue200 = Color(0xFF81D4FA) + val LightBlue300 = Color(0xFF4FC3F7) + val LightBlue400 = Color(0xFF29B6F6) + val LightBlue500 = Color(0xFF03A9F4) + val LightBlue600 = Color(0xFF039BE5) + val LightBlue700 = Color(0xFF0288D1) + val LightBlue800 = Color(0xFF0277BD) + val LightBlue900 = Color(0xFF01579B) + + val Cyan50 = Color(0xFFE0F7FA) + val Cyan100 = Color(0xFFB2EBF2) + val Cyan200 = Color(0xFF80DEEA) + val Cyan300 = Color(0xFF4DD0E1) + val Cyan400 = Color(0xFF26C6DA) + val Cyan500 = Color(0xFF00BCD4) + val Cyan600 = Color(0xFF00ACC1) + val Cyan700 = Color(0xFF0097A7) + val Cyan800 = Color(0xFF00838F) + val Cyan900 = Color(0xFF006064) + + val Teal50 = Color(0xFFE0F2F1) + val Teal100 = Color(0xFFB2DFDB) + val Teal200 = Color(0xFF80CBC4) + val Teal300 = Color(0xFF4DB6AC) + val Teal400 = Color(0xFF26A69A) + val Teal500 = Color(0xFF009688) + val Teal600 = Color(0xFF00897B) + val Teal700 = Color(0xFF00796B) + val Teal800 = Color(0xFF00695C) + val Teal900 = Color(0xFF004D40) + + val LightGreen50 = Color(0xFFF1F8E9) + val LightGreen100 = Color(0xFFDCEDC8) + val LightGreen200 = Color(0xFFC5E1A5) + val LightGreen300 = Color(0xFFAED581) + val LightGreen400 = Color(0xFF9CCC65) + val LightGreen500 = Color(0xFF8BC34A) + val LightGreen600 = Color(0xFF7CB342) + val LightGreen700 = Color(0xFF689F38) + val LightGreen800 = Color(0xFF558B2F) + val LightGreen900 = Color(0xFF33691E) + + val Green50 = Color(0xFFE8F5E9) + val Green100 = Color(0xFFC8E6C9) + val Green200 = Color(0xFFA5D6A7) + val Green300 = Color(0xFF81C784) + val Green400 = Color(0xFF66BB6A) + val Green500 = Color(0xFF4CAF50) + val Green600 = Color(0xFF43A047) + val Green700 = Color(0xFF388E3C) + val Green800 = Color(0xFF2E7D32) + val Green900 = Color(0xFF1B5E20) + + val Lime50 = Color(0xFFF9FBE7) + val Lime100 = Color(0xFFF0F4C3) + val Lime200 = Color(0xFFE6EE9C) + val Lime300 = Color(0xFFDCE775) + val Lime400 = Color(0xFFD4E157) + val Lime500 = Color(0xFFCDDC39) + val Lime600 = Color(0xFFC0CA33) + val Lime700 = Color(0xFFAFB42B) + val Lime800 = Color(0xFF9E9D24) + val Lime900 = Color(0xFF827717) + + val Yellow50 = Color(0xFFFFFDE7) + val Yellow100 = Color(0xFFFFF9C4) + val Yellow200 = Color(0xFFFFF59D) + val Yellow300 = Color(0xFFFFF176) + val Yellow400 = Color(0xFFFFEE58) + val Yellow500 = Color(0xFFFFEB3B) + val Yellow600 = Color(0xFFFDD835) + val Yellow700 = Color(0xFFFBC02D) + val Yellow800 = Color(0xFFF9A825) + val Yellow900 = Color(0xFFF57F17) + + val Amber50 = Color(0xFFFFF8E1) + val Amber100 = Color(0xFFFFECB3) + val Amber200 = Color(0xFFFFE082) + val Amber300 = Color(0xFFFFD54F) + val Amber400 = Color(0xFFFFCA28) + val Amber500 = Color(0xFFFFC107) + val Amber600 = Color(0xFFFFB300) + val Amber700 = Color(0xFFFFA000) + val Amber800 = Color(0xFFFF8F00) + val Amber900 = Color(0xFFFF6F00) + + val Orange50 = Color(0xFFFFF3E0) + val Orange100 = Color(0xFFFFE0B2) + val Orange200 = Color(0xFFFFCC80) + val Orange300 = Color(0xFFFFB74D) + val Orange400 = Color(0xFFFFA726) + val Orange500 = Color(0xFFFF9800) + val Orange600 = Color(0xFFFB8C00) + val Orange700 = Color(0xFFF57C00) + val Orange800 = Color(0xFFEF6C00) + val Orange900 = Color(0xFFE65100) + + val DeepOrange50 = Color(0xFFFBE9E7) + val DeepOrange100 = Color(0xFFFFCCBC) + val DeepOrange200 = Color(0xFFFFAB91) + val DeepOrange300 = Color(0xFFFF8A65) + val DeepOrange400 = Color(0xFFFF7043) + val DeepOrange500 = Color(0xFFFF5722) + val DeepOrange600 = Color(0xFFF4511E) + val DeepOrange700 = Color(0xFFE64A19) + val DeepOrange800 = Color(0xFFD84315) + val DeepOrange900 = Color(0xFFBF360C) + + val Brown50 = Color(0xFFEFEBE9) + val Brown100 = Color(0xFFD7CCC8) + val Brown200 = Color(0xFFBCAAA4) + val Brown300 = Color(0xFFA1887F) + val Brown400 = Color(0xFF8D6E63) + val Brown500 = Color(0xFF795548) + val Brown600 = Color(0xFF6D4C41) + val Brown700 = Color(0xFF5D4037) + val Brown800 = Color(0xFF4E342E) + val Brown900 = Color(0xFF3E2723) + + val Gray50 = Color(0xFFFAFAFA) + val Gray100 = Color(0xFFF5F5F5) + val Gray200 = Color(0xFFEEEEEE) + val Gray300 = Color(0xFFE0E0E0) + val Gray400 = Color(0xFFBDBDBD) + val Gray500 = Color(0xFF9E9E9E) + val Gray600 = Color(0xFF757575) + val Gray700 = Color(0xFF616161) + val Gray800 = Color(0xFF424242) + val Gray900 = Color(0xFF212121) + + val BlueGrey50 = Color(0xFFECEFF1) + val BlueGrey100 = Color(0xFFCFD8DC) + val BlueGrey200 = Color(0xFFB0BEC5) + val BlueGrey300 = Color(0xFF90A4AE) + val BlueGrey400 = Color(0xFF78909C) + val BlueGrey500 = Color(0xFF607D8B) + val BlueGrey600 = Color(0xFF546E7A) + val BlueGrey700 = Color(0xFF455A64) + val BlueGrey800 = Color(0xFF37474F) + val BlueGrey900 = Color(0xFF263238) +} diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_1.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_1.jpg new file mode 100644 index 000000000..65be51c6b Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_1.jpg differ diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_2.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_2.jpg new file mode 100644 index 000000000..ae83e1ac6 Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_2.jpg differ diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_3.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_3.jpg new file mode 100644 index 000000000..362ef3874 Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_3.jpg differ diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_4.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_4.jpg new file mode 100644 index 000000000..052ef4c72 Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_4.jpg differ diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_5.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_5.jpg new file mode 100644 index 000000000..137a7f3a1 Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_5.jpg differ diff --git a/compose/snippets/src/main/res/drawable-v24/ic_launcher_foreground.xml b/compose/snippets/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..0f6026a41 --- /dev/null +++ b/compose/snippets/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/compose/snippets/src/main/res/drawable/baseline_directions_bus_24.xml b/compose/snippets/src/main/res/drawable/baseline_directions_bus_24.xml new file mode 100644 index 000000000..745eab18a --- /dev/null +++ b/compose/snippets/src/main/res/drawable/baseline_directions_bus_24.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/baseline_shopping_cart_24.xml b/compose/snippets/src/main/res/drawable/baseline_shopping_cart_24.xml new file mode 100644 index 000000000..15368afa6 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/baseline_shopping_cart_24.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/button_outline.xml b/compose/snippets/src/main/res/drawable/button_outline.xml new file mode 100644 index 000000000..790fd2eb2 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/button_outline.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/compose/snippets/src/main/res/drawable/cupcake.jpg b/compose/snippets/src/main/res/drawable/cupcake.jpg new file mode 100644 index 000000000..0696e8a64 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/cupcake.jpg differ diff --git a/compose/snippets/src/main/res/drawable/dog.jpg b/compose/snippets/src/main/res/drawable/dog.jpg new file mode 100644 index 000000000..800153d4f Binary files /dev/null and b/compose/snippets/src/main/res/drawable/dog.jpg differ diff --git a/compose/snippets/src/main/res/drawable/donut.jpeg b/compose/snippets/src/main/res/drawable/donut.jpeg new file mode 100644 index 000000000..57d0199f2 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/donut.jpeg differ diff --git a/compose/snippets/src/main/res/drawable/eclair.jpeg b/compose/snippets/src/main/res/drawable/eclair.jpeg new file mode 100644 index 000000000..6ec767a0f Binary files /dev/null and b/compose/snippets/src/main/res/drawable/eclair.jpeg differ diff --git a/compose/snippets/src/main/res/drawable/fast_forward.xml b/compose/snippets/src/main/res/drawable/fast_forward.xml new file mode 100644 index 000000000..95cdea9c4 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/fast_forward.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/fast_forward_filled.xml b/compose/snippets/src/main/res/drawable/fast_forward_filled.xml new file mode 100644 index 000000000..84a42ad80 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/fast_forward_filled.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/fast_rewind.xml b/compose/snippets/src/main/res/drawable/fast_rewind.xml new file mode 100644 index 000000000..4564c5ba7 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/fast_rewind.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/fast_rewind_filled.xml b/compose/snippets/src/main/res/drawable/fast_rewind_filled.xml new file mode 100644 index 000000000..f6f8cebb1 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/fast_rewind_filled.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/favorite.xml b/compose/snippets/src/main/res/drawable/favorite.xml new file mode 100644 index 000000000..e6859af3e --- /dev/null +++ b/compose/snippets/src/main/res/drawable/favorite.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/favorite_filled.xml b/compose/snippets/src/main/res/drawable/favorite_filled.xml new file mode 100644 index 000000000..d49f76c61 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/favorite_filled.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/feathertop.jpeg b/compose/snippets/src/main/res/drawable/feathertop.jpeg new file mode 100644 index 000000000..65058c722 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/feathertop.jpeg differ diff --git a/compose/snippets/src/main/res/drawable/froyo.jpeg b/compose/snippets/src/main/res/drawable/froyo.jpeg new file mode 100644 index 000000000..f3126d507 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/froyo.jpeg differ diff --git a/compose/snippets/src/main/res/drawable/gingerbread.jpg b/compose/snippets/src/main/res/drawable/gingerbread.jpg new file mode 100644 index 000000000..8345d47e9 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/gingerbread.jpg differ diff --git a/compose/snippets/src/main/res/drawable/hero.png b/compose/snippets/src/main/res/drawable/hero.png new file mode 100644 index 000000000..be4f76e58 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/hero.png differ diff --git a/compose/snippets/src/main/res/drawable/honeycomb.jpg b/compose/snippets/src/main/res/drawable/honeycomb.jpg new file mode 100644 index 000000000..94c892b57 Binary files /dev/null and b/compose/snippets/src/main/res/drawable/honeycomb.jpg differ diff --git a/compose/snippets/src/main/res/drawable/ic_hourglass_animated.xml b/compose/snippets/src/main/res/drawable/ic_hourglass_animated.xml new file mode 100644 index 000000000..c30879c0e --- /dev/null +++ b/compose/snippets/src/main/res/drawable/ic_hourglass_animated.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compose/snippets/src/main/res/drawable/ic_launcher_background.xml b/compose/snippets/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..2edadf928 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compose/snippets/src/main/res/drawable/ic_logo.xml b/compose/snippets/src/main/res/drawable/ic_logo.xml new file mode 100644 index 000000000..9937f5517 --- /dev/null +++ b/compose/snippets/src/main/res/drawable/ic_logo.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/ic_moon_24.xml b/compose/snippets/src/main/res/drawable/ic_moon_24.xml new file mode 100644 index 000000000..b1c551d5e --- /dev/null +++ b/compose/snippets/src/main/res/drawable/ic_moon_24.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/ic_sun_24.xml b/compose/snippets/src/main/res/drawable/ic_sun_24.xml new file mode 100644 index 000000000..4bac89d3e --- /dev/null +++ b/compose/snippets/src/main/res/drawable/ic_sun_24.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/compose/snippets/src/main/res/drawable/rainbow.jpeg b/compose/snippets/src/main/res/drawable/rainbow.jpeg new file mode 100644 index 000000000..6b27dc2ff Binary files /dev/null and b/compose/snippets/src/main/res/drawable/rainbow.jpeg differ diff --git a/compose/snippets/src/main/res/drawable/sunset.jpg b/compose/snippets/src/main/res/drawable/sunset.jpg new file mode 100644 index 000000000..9b3c7ff0e Binary files /dev/null and b/compose/snippets/src/main/res/drawable/sunset.jpg differ diff --git a/compose/snippets/src/main/res/font/raleway_light.ttf b/compose/snippets/src/main/res/font/raleway_light.ttf new file mode 100755 index 000000000..b5ec48606 Binary files /dev/null and b/compose/snippets/src/main/res/font/raleway_light.ttf differ diff --git a/compose/snippets/src/main/res/font/raleway_medium.ttf b/compose/snippets/src/main/res/font/raleway_medium.ttf new file mode 100755 index 000000000..070ac7691 Binary files /dev/null and b/compose/snippets/src/main/res/font/raleway_medium.ttf differ diff --git a/compose/snippets/src/main/res/font/raleway_regular.ttf b/compose/snippets/src/main/res/font/raleway_regular.ttf new file mode 100755 index 000000000..746c24238 Binary files /dev/null and b/compose/snippets/src/main/res/font/raleway_regular.ttf differ diff --git a/compose/snippets/src/main/res/font/raleway_semibold.ttf b/compose/snippets/src/main/res/font/raleway_semibold.ttf new file mode 100755 index 000000000..34db42061 Binary files /dev/null and b/compose/snippets/src/main/res/font/raleway_semibold.ttf differ diff --git a/compose/snippets/src/main/res/font/robotoflex_static_regular.ttf b/compose/snippets/src/main/res/font/robotoflex_static_regular.ttf new file mode 100644 index 000000000..f857ae9c5 Binary files /dev/null and b/compose/snippets/src/main/res/font/robotoflex_static_regular.ttf differ diff --git a/compose/snippets/src/main/res/font/robotoflex_variable.ttf b/compose/snippets/src/main/res/font/robotoflex_variable.ttf new file mode 100644 index 000000000..c39aafb25 Binary files /dev/null and b/compose/snippets/src/main/res/font/robotoflex_variable.ttf differ diff --git a/compose/snippets/src/main/res/layout/activity_example.xml b/compose/snippets/src/main/res/layout/activity_example.xml new file mode 100644 index 000000000..b85c61cf1 --- /dev/null +++ b/compose/snippets/src/main/res/layout/activity_example.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/compose/snippets/src/main/res/layout/example_layout.xml b/compose/snippets/src/main/res/layout/example_layout.xml new file mode 100644 index 000000000..91e49c045 --- /dev/null +++ b/compose/snippets/src/main/res/layout/example_layout.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/compose/snippets/src/main/res/layout/example_view.xml b/compose/snippets/src/main/res/layout/example_view.xml new file mode 100644 index 000000000..86bdacae8 --- /dev/null +++ b/compose/snippets/src/main/res/layout/example_view.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/compose/snippets/src/main/res/layout/fragment_example.xml b/compose/snippets/src/main/res/layout/fragment_example.xml new file mode 100644 index 000000000..d5517e3c4 --- /dev/null +++ b/compose/snippets/src/main/res/layout/fragment_example.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/compose/snippets/src/main/res/layout/interop_layout_preview_composable.xml b/compose/snippets/src/main/res/layout/interop_layout_preview_composable.xml new file mode 100644 index 000000000..b3a672cbe --- /dev/null +++ b/compose/snippets/src/main/res/layout/interop_layout_preview_composable.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/compose/snippets/src/main/res/layout/migration_strategy_existing_screens.xml b/compose/snippets/src/main/res/layout/migration_strategy_existing_screens.xml new file mode 100644 index 000000000..a7bd893b7 --- /dev/null +++ b/compose/snippets/src/main/res/layout/migration_strategy_existing_screens.xml @@ -0,0 +1,33 @@ + + + + + + + + + + diff --git a/compose/snippets/src/main/res/layout/migration_strategy_xml_example.xml b/compose/snippets/src/main/res/layout/migration_strategy_xml_example.xml new file mode 100644 index 000000000..e5ed1b289 --- /dev/null +++ b/compose/snippets/src/main/res/layout/migration_strategy_xml_example.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + +