Showing preview only (561K chars total). Download the full file or copy to clipboard to get everything.
Repository: google/accompanist
Branch: main
Commit: 12ec3408fc10
Files: 157
Total size: 515.2 KB
Directory structure:
gitextract_cgrc05nu/
├── .allstar/
│ └── binary_artifacts.yaml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── adaptive-bug-report.md
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ ├── general-bug-report.md
│ │ ├── general-other-bug-report.md
│ │ ├── navigation-material-bug-report.md
│ │ ├── permissions-bug-report.md
│ │ └── testharness-bug-report.md
│ ├── auto-merge.yml
│ ├── ci-gradle.properties
│ ├── pull_request_template.md
│ ├── release-drafter.yml
│ └── workflows/
│ ├── automerger.yml
│ ├── build-snapshot.yml
│ ├── build.yml
│ ├── issues-stale.yml
│ ├── publish-docs.yml
│ └── update-release.yml
├── .gitignore
├── .idea/
│ ├── codeStyles/
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── copyright/
│ │ ├── AOSP.xml
│ │ └── profiles_settings.xml
│ ├── inspectionProfiles/
│ │ ├── ktlint.xml
│ │ └── profiles_settings.xml
│ ├── kotlinScripting.xml
│ ├── kotlinc.xml
│ ├── runConfigurations.xml
│ └── vcs.xml
├── ASSETS_LICENSE.txt
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── adaptive/
│ ├── README.md
│ ├── api/
│ │ └── current.api
│ ├── build.gradle.kts
│ ├── gradle.properties
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── adaptive/
│ │ ├── DisplayFeatures.kt
│ │ ├── FoldAwareColumn.kt
│ │ ├── FoldAwareColumnScope.kt
│ │ ├── RowColumnImpl.kt
│ │ ├── RowColumnMeasurementHelper.kt
│ │ └── TwoPane.kt
│ ├── sharedTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── adaptive/
│ │ ├── DisplayFeaturesTest.kt
│ │ ├── FoldAwareColumnTest.kt
│ │ └── TwoPaneTest.kt
│ └── test/
│ └── resources/
│ └── robolectric.properties
├── build-logic/
│ ├── convention/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── kotlin/
│ │ ├── AndroidLibraryComposeConventionPlugin.kt
│ │ ├── AndroidLibraryConventionPlugin.kt
│ │ ├── AndroidLibraryPublishedConventionPlugin.kt
│ │ ├── AndroidLintConventionPlugin.kt
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ ├── AndroidCompose.kt
│ │ ├── BundleInsideHelper.kt
│ │ ├── KotlinAndroid.kt
│ │ └── ProjectExtensions.kt
│ ├── gradle.properties
│ └── settings.gradle.kts
├── build.gradle
├── checksum.sh
├── docs/
│ ├── adaptive.md
│ ├── appcompat-theme.md
│ ├── drawablepainter.md
│ ├── migration.md
│ ├── navigation-animation.md
│ ├── navigation-material.md
│ ├── permissions.md
│ ├── systemuicontroller.md
│ ├── updating.md
│ ├── using-snapshot-version.md
│ └── web.md
├── drawablepainter/
│ ├── README.md
│ ├── api/
│ │ └── current.api
│ ├── build.gradle.kts
│ ├── gradle.properties
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── google/
│ └── accompanist/
│ └── drawablepainter/
│ └── DrawablePainter.kt
├── generate_docs.sh
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── images/
│ └── Social.sketch
├── internal-testutils/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── internal/
│ │ └── test/
│ │ ├── ActivityScenario.kt
│ │ ├── Assertions.kt
│ │ ├── IgnoreOnRobolectric.kt
│ │ ├── TestUtils.kt
│ │ └── WaitUntil.kt
│ └── res/
│ └── values/
│ └── themes.xml
├── mkdocs.yml
├── permissions/
│ ├── README.md
│ ├── api/
│ │ └── current.api
│ ├── build.gradle.kts
│ ├── gradle.properties
│ └── src/
│ ├── androidTest/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── permissions/
│ │ ├── FakeTests.kt
│ │ ├── MultipleAndSinglePermissionsTest.kt
│ │ ├── MultiplePermissionsStateTest.kt
│ │ ├── PermissionStateTest.kt
│ │ ├── RequestMultiplePermissionsTest.kt
│ │ ├── RequestPermissionTest.kt
│ │ ├── TestUtils.kt
│ │ └── test/
│ │ ├── EmptyPermissionsTestActivity.kt
│ │ └── PermissionsTestActivity.kt
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── google/
│ └── accompanist/
│ └── permissions/
│ ├── MultiplePermissionsState.kt
│ ├── MutableMultiplePermissionsState.kt
│ ├── MutablePermissionState.kt
│ ├── PermissionState.kt
│ └── PermissionsUtil.kt
├── permissions-lint/
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── google/
│ │ │ └── accompanist/
│ │ │ └── permissions/
│ │ │ └── lint/
│ │ │ ├── PermissionsIssueRegistry.kt
│ │ │ ├── PermissionsLaunchDetector.kt
│ │ │ └── util/
│ │ │ ├── ComposableUtils.kt
│ │ │ ├── KotlinMetadataUtils.kt
│ │ │ ├── Names.kt
│ │ │ └── PsiUtils.kt
│ │ └── resources/
│ │ └── META-INF/
│ │ └── services/
│ │ └── com.android.tools.lint.client.api.IssueRegistry
│ └── test/
│ └── java/
│ └── com/
│ └── google/
│ └── accompanist/
│ └── permissions/
│ └── lint/
│ └── PermissionsLaunchDetectorTest.kt
├── release/
│ ├── secring.gpg.aes
│ ├── signing-cleanup.sh
│ ├── signing-setup.sh
│ └── signing.properties.aes
├── sample/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── sample/
│ │ ├── ImageLoadingSampleUtils.kt
│ │ ├── MainActivity.kt
│ │ ├── MainScreen.kt
│ │ ├── Theme.kt
│ │ ├── adaptive/
│ │ │ ├── BasicTwoPaneSample.kt
│ │ │ ├── DraggableFoldAwareColumnSample.kt
│ │ │ ├── HorizontalTwoPaneSample.kt
│ │ │ ├── NavDrawerFoldAwareColumnSample.kt
│ │ │ ├── NavRailFoldAwareColumnSample.kt
│ │ │ └── VerticalTwoPaneSample.kt
│ │ ├── drawablepainter/
│ │ │ └── DocSamples.kt
│ │ └── permissions/
│ │ ├── RequestLocationPermissionsSample.kt
│ │ ├── RequestMultiplePermissionsSample.kt
│ │ └── RequestPermissionSample.kt
│ └── res/
│ ├── drawable/
│ │ ├── ic_launcher_background.xml
│ │ └── rectangle.xml
│ ├── drawable-v24/
│ │ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── values/
│ │ └── strings.xml
│ └── values-ar/
│ └── strings.xml
├── scripts/
│ └── run-tests.sh
├── settings.gradle.kts
└── spotless/
├── copyright.txt
└── greclipse.properties
================================================
FILE CONTENTS
================================================
================================================
FILE: .allstar/binary_artifacts.yaml
================================================
# Exemption reason: This repo uses binary artifacts to ship gradle.jar for users. It does not allow any others.
# Exemption timeframe: permanent
optConfig:
optOut: true
================================================
FILE: .github/ISSUE_TEMPLATE/adaptive-bug-report.md
================================================
---
name: Adaptive bug report
about: Create a report about adaptive
title: "[Adaptive]"
labels: adaptive
assignees: alexvanyo
---
**Description**
**Steps to reproduce**
**Expected behavior**
**Additional context**
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior
A clear and concise description of what you expected to happen.
## Screenshots?
If applicable, add screenshots to help explain your problem.
## Environment:
- Android OS version: [e.g. Android 5.0]
- Device: [e.g. Emulator, Google Pixel 4]
- Accompanist version: [e.g. v0.X]
## Additional context
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
================================================
FILE: .github/ISSUE_TEMPLATE/general-bug-report.md
================================================
---
name: General Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce (if applicable)
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior (if applicable)
A clear and concise description of what you expected to happen.
## Screenshots? (if applicable)
If applicable, add screenshots to help explain your problem.
## Environment: (if applicable)
- Android OS version: [e.g. Android 5.0]
- Device: [e.g. Emulator, Google Pixel 4]
- Accompanist version: [e.g. v0.X]
## Additional context
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/general-other-bug-report.md
================================================
---
name: General/Other bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce (if applicable)
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior (if applicable)
A clear and concise description of what you expected to happen.
## Screenshots? (if applicable)
If applicable, add screenshots to help explain your problem.
## Environment: (if applicable)
- Android OS version: [e.g. Android 5.0]
- Device: [e.g. Emulator, Google Pixel 4]
- Accompanist version: [e.g. v0.X]
## Additional context
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/navigation-material-bug-report.md
================================================
---
name: Navigation Material bug report
about: Create a report to help us improve
title: "[Navigation Material] "
labels: ''
assignees: jossiwolf
---
**Description**
**Steps to reproduce**
**Expected behavior**
**Additional context**
================================================
FILE: .github/ISSUE_TEMPLATE/permissions-bug-report.md
================================================
---
name: Permissions bug report
about: Create a report to help us improve
title: "[Permissions] "
labels: ''
assignees: bentrengrove
---
**Description**
**Steps to reproduce**
**Expected behavior**
**Additional context**
================================================
FILE: .github/ISSUE_TEMPLATE/testharness-bug-report.md
================================================
---
name: Test harness bug report
about: Create a report about test harness
title: "[Test Harness]"
labels: testharness
assignees: alexvanyo
---
**Description**
**Steps to reproduce**
**Expected behavior**
**Additional context**
================================================
FILE: .github/auto-merge.yml
================================================
# Config for github.com/bobvanderlinden/probot-auto-merge
minApprovals:
COLLABORATOR: 1
requiredLabels:
- automerge
mergeMethod: merge
reportStatus: true
================================================
FILE: .github/ci-gradle.properties
================================================
#
# Copyright 2019 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.
#
# Turn Gradle daemon off due to https://github.com/Kotlin/dokka/issues/1405
org.gradle.daemon=false
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4608m -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.workers.max=2
kotlin.compiler.execution.strategy=in-process
================================================
FILE: .github/pull_request_template.md
================================================
### Please add the library name to the PR title. Example: "[Insets] Fixes typo" ###
================================================
FILE: .github/release-drafter.yml
================================================
name-template: 'v$NEXT_PATCH_VERSION 🌈'
tag-template: 'v$NEXT_PATCH_VERSION'
template: |
## What’s Changed
$CHANGES
================================================
FILE: .github/workflows/automerger.yml
================================================
name: main to snapshot auto merger
on:
push:
branches:
- main
jobs:
automerge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: '0' # 0 == fetch all history history
ref: 'snapshot'
token: ${{ secrets.AUTOMERGE_PAT }}
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
git fetch origin
git merge origin/main --no-edit
git push
================================================
FILE: .github/workflows/build-snapshot.yml
================================================
name: Build & test (snapshot)
on:
push:
branches:
- snapshot
paths-ignore:
- '**.md'
pull_request:
branches:
- snapshot
workflow_dispatch:
jobs:
build:
# Skip build if head commit contains 'skip ci'
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v2
with:
# Fetch expanded history, which is needed for affected module detection
fetch-depth: '500'
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: set up JDK
uses: actions/setup-java@v1
with:
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Generate cache key
run: ./checksum.sh checksum.txt
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches/modules-*
~/.gradle/caches/jars-*
~/.gradle/caches/build-cache-*
key: gradle-${{ hashFiles('checksum.txt') }}
- name: Build
run: |
./gradlew --scan --stacktrace \
spotlessCheck \
assemble \
metalavaCheckCompatibilityRelease \
lintDebug
- name: Unit Tests
run: |
./scripts/run-tests.sh \
--unit-tests \
--run-affected \
--affected-base-ref=$BASE_REF
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-robolectric
path: |
**/build/test-results/*
**/build/reports/*
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
test:
runs-on: macos-latest
needs: build
timeout-minutes: 50
strategy:
# Allow tests to continue on other devices if they fail on one device.
fail-fast: false
matrix:
api-level: [ 22, 26, 29, 31, 32 ]
shard: [ 0, 1 ] # Need to update shard-count below if this changes
env:
TERM: dumb
steps:
- uses: actions/checkout@v2
with:
# Fetch expanded history, which is needed for affected module detection
fetch-depth: '500'
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: set up JDK
uses: actions/setup-java@v1
with:
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Generate cache key
run: ./checksum.sh checksum.txt
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches/modules-*
~/.gradle/caches/jars-*
~/.gradle/caches/build-cache-*
key: gradle-${{ hashFiles('checksum.txt') }}
# Determine what emulator image to use. We run all API 28+ emulators using
# the google_apis image
- name: Determine emulator target
id: determine-target
env:
API_LEVEL: ${{ matrix.api-level }}
run: |
TARGET="default"
if [ "$API_LEVEL" -ge "28" ]; then
TARGET="google_apis"
fi
echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ steps.determine-target.outputs.TARGET }}
profile: Galaxy Nexus
script: ./scripts/run-tests.sh --log-file=logcat.txt --run-affected --affected-base-ref=$BASE_REF --shard-index=${{ matrix.shard }} --shard-count=2
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
- name: Upload logs
if: always()
uses: actions/upload-artifact@v2
with:
name: logs-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }}
path: logcat.txt
- name: Upload test results
if: always()
uses: actions/upload-artifact@v2
with:
name: test-results-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }}
path: |
**/build/reports/*
**/build/outputs/*/connected/*
deploy:
if: github.event_name == 'push' # only deploy for pushed commits (not PRs)
runs-on: ubuntu-latest
needs: [ build, test ]
timeout-minutes: 30
env:
TERM: dumb
steps:
- uses: actions/checkout@v2
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: set up JDK
uses: actions/setup-java@v1
with:
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Generate cache key
run: ./checksum.sh checksum.txt
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches/modules-*
~/.gradle/caches/jars-*
~/.gradle/caches/build-cache-*
key: gradle-${{ hashFiles('checksum.txt') }}
- name: Deploy to Sonatype
run: ./gradlew publish --no-parallel --stacktrace --no-configuration-cache
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
================================================
FILE: .github/workflows/build.yml
================================================
name: Build & test
on:
push:
branches:
- main
- compose-1.0
- compose-1.1
- compose-1.2
- compose-1.3
- compose-1.4
- compose-1.5
- compose-1.6
paths-ignore:
- '**.md'
pull_request:
jobs:
build:
# Skip build if head commit contains 'skip ci'
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v2
with:
# Fetch expanded history, which is needed for affected module detection
fetch-depth: '500'
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Build
run: |
./gradlew --scan --stacktrace \
spotlessCheck \
assemble \
metalavaCheckCompatibilityRelease \
lintDebug
- name: Unit Tests
run: |
./scripts/run-tests.sh \
--unit-tests \
--run-affected \
--affected-base-ref=$BASE_REF
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-robolectric
path: |
**/build/test-results/*
**/build/reports/*
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
test:
runs-on: ubuntu-latest
needs: build
timeout-minutes: 70
strategy:
# Allow tests to continue on other devices if they fail on one device.
fail-fast: false
matrix:
api-level: [ 22, 26, 30 ]
shard: [ 0, 1 ] # Need to update shard-count below if this changes
env:
TERM: dumb
steps:
- uses: actions/checkout@v3
with:
# Fetch expanded history, which is needed for affected module detection
fetch-depth: '500'
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
# Determine what emulator image to use. We run all API 28+ emulators using
# the google_apis image
- name: Determine emulator target
id: determine-target
env:
API_LEVEL: ${{ matrix.api-level }}
run: |
TARGET="default"
if [ "$API_LEVEL" -ge "28" ]; then
TARGET="google_apis"
fi
echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
- name: Determine emulator arch
id: determine-arch
env:
API_LEVEL: ${{ matrix.api-level }}
run: |
ARCH="x86"
if [ "$API_LEVEL" -ge "29" ]; then
ARCH="x86_64"
fi
echo "ARCH=$ARCH" >> $GITHUB_OUTPUT
- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: ${{ steps.determine-arch.outputs.ARCH }}
target: ${{ steps.determine-target.outputs.TARGET }}
profile: Galaxy Nexus
script: ./scripts/run-tests.sh --log-file=logcat.txt --run-affected --affected-base-ref=$BASE_REF --shard-index=${{ matrix.shard }} --shard-count=2
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: logs-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }}
path: logcat.txt
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.api-level }}-${{ steps.determine-target.outputs.TARGET }}-${{ matrix.shard }}
path: |
**/build/reports/*
**/build/outputs/*/connected/*
deploy:
if: github.event_name == 'push' # only deploy for pushed commits (not PRs)
runs-on: ubuntu-latest
needs: [ build, test ]
timeout-minutes: 30
env:
TERM: dumb
steps:
- uses: actions/checkout@v2
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Decrypt secrets
run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Deploy to Sonatype
run: ./gradlew publish --no-parallel --stacktrace --no-configuration-cache
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
- name: Clean secrets
if: always()
run: release/signing-cleanup.sh
================================================
FILE: .github/workflows/issues-stale.yml
================================================
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '15 3 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
days-before-stale: 45
days-before-close: 5
exempt-all-pr-milestones: true
exempt-issue-labels: 'waiting for info,waiting on dependency'
================================================
FILE: .github/workflows/publish-docs.yml
================================================
name: Publish docs
on:
push:
tags:
- v*
workflow_dispatch:
jobs:
deploy_docs:
runs-on: ubuntu-latest
env:
TERM: dumb
steps:
- uses: actions/checkout@v2
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Setup java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install mkdocs-material=="9.*"
- name: Generate docs
run: ./generate_docs.sh
- name: Build site
run: mkdocs build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
================================================
FILE: .github/workflows/update-release.yml
================================================
name: Update release
on:
push:
branches:
- main
jobs:
update_draft_release:
runs-on: ubuntu-latest
steps:
# pin directly to 6.0.0 because we don't want to update without knowledge
- uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# Gradle
.gradle
build/
captures
/local.properties
# IntelliJ .idea folder
.idea/workspace.xml
.idea/libraries
.idea/caches
.idea/navEditor.xml
.idea/tasks.xml
.idea/modules.xml
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/deploymentTargetDropDown.xml
.idea/misc.xml
.idea/androidTestResultsUserPreferences.xml
.idea/deploymentTargetSelector.xml
gradle.xml
*.iml
# General
.DS_Store
.externalNativeBuild
# Do not commit plain-text signing info
release/*.properties
release/*.gpg
# VS Code config
org.eclipse.buildship.core.prefs
.classpath
.project
# Temporary API docs
docs/api
package-list-coil-base
# Mkdocs temporary serving folder
docs-gen
site
*.bak
# Lint reports
lint-report.*
================================================
FILE: .idea/codeStyles/Project.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="99" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="99" />
<option name="CONTINUATION_INDENT_IN_PARAMETER_LISTS" value="true" />
<option name="CONTINUATION_INDENT_IN_ARGUMENT_LISTS" value="true" />
<option name="CONTINUATION_INDENT_FOR_EXPRESSION_BODIES" value="true" />
<option name="CONTINUATION_INDENT_FOR_CHAINED_CALLS" value="true" />
<option name="CONTINUATION_INDENT_IN_SUPERTYPE_LISTS" value="true" />
<option name="CONTINUATION_INDENT_IN_IF_CONDITIONS" value="true" />
<option name="CONTINUATION_INDENT_IN_ELVIS" value="true" />
<option name="WRAP_EXPRESSION_BODY_FUNCTIONS" value="0" />
<option name="IF_RPAREN_ON_NEW_LINE" value="false" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
================================================
FILE: .idea/copyright/AOSP.xml
================================================
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright &#36;today.year 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." />
<option name="myName" value="AOSP" />
</copyright>
</component>
================================================
FILE: .idea/copyright/profiles_settings.xml
================================================
<component name="CopyrightManager">
<settings default="AOSP" />
</component>
================================================
FILE: .idea/inspectionProfiles/ktlint.xml
================================================
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="ktlint" />
<inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>
================================================
FILE: .idea/inspectionProfiles/profiles_settings.xml
================================================
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="ktlint" />
<version value="1.0" />
</settings>
</component>
================================================
FILE: .idea/kotlinScripting.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinScriptingSettings">
<option name="isAutoReloadEnabled" value="true" />
</component>
</project>
================================================
FILE: .idea/kotlinc.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.20" />
</component>
</project>
================================================
FILE: .idea/runConfigurations.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
================================================
FILE: ASSETS_LICENSE.txt
================================================
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.
================================================
FILE: CONTRIBUTING.md
================================================
# How to Contribute
We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.
## New Features/Libraries
Before contributing large new features and/or libraries please start a discussion
with us first via GitHub Issues and check that we can support it.
We are unable to support all new features, even though we wish we could! If we
are unable to support adding your feature, we always encourage you to open source it
in your own repository to help the Compose community grow.
## Contributor License Agreement
Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution,
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.
You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
## Code Reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## API Changes
If you are changing any public APIs, you need to run `./gradlew metalavaGenerateSignatureRelease` which will
update the API signatures.
## Formatting
To apply formatting, we use spotless. Run `./gradlew :pager:spotlessApply` to format the code according
to the spec.
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
================================================
FILE: README.md
================================================

Accompanist is a group of libraries that aim to supplement [Jetpack Compose][compose] with features that are commonly required by developers but not yet available.
Accompanist is a labs like environment for new Compose APIs. We use it to help fill known gaps in the Compose toolkit, experiment with new APIs and to gather insight into the development experience of developing a Compose library. The goal of these libraries is to upstream them into the official toolkit, at which point they will be deprecated and removed from Accompanist.
For more details like, why does this library exist? Why is it not part of AndroidX? Will you be releasing more libraries? Check out our [Accompanist FAQ](https://medium.com/p/b55117b02712).
## Compose versions
Each [release](https://github.com/google/accompanist/releases) outlines what version of the Compose UI libraries it depends on. We are currently releasing multiple versions of Accompanist for the different versions of Compose:
<table>
<tr>
<td>Compose 1.0 (1.0.x)</td><td><img alt="Maven Central" src="https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-permissions?versionPrefix=0.20"></td>
</tr>
<tr>
<td>Compose 1.1 (1.1.x)</td><td><img alt="Maven Central" src="https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-permissions?versionPrefix=0.23"></td>
</tr>
<tr>
<td>Compose UI 1.2 (1.2.x)</td><td><img alt="Maven Central" src="https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-permissions?versionPrefix=0.25"></td>
</tr>
<tr>
<td>Compose UI 1.3 (1.3.x)</td><td><img alt="Maven Central" src="https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-permissions?versionPrefix=0.28"></td>
</tr>
<tr>
<td>Compose UI 1.4 (1.4.x)</td><td><img alt="Maven Central" src="https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-permissions?versionPrefix=0.30"></td>
</tr>
<tr>
<td>Compose UI 1.5 (1.5.x)</td><td><img alt="Maven Central" src="https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-permissions?versionPrefix=0.32"></td>
</tr>
<tr>
<td>Compose UI 1.6 (1.6.x)</td><td><img alt="Maven Central" src="https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-permissions?versionPrefix=0.34"></td>
</tr>
<tr>
<td>Compose UI 1.7+ (1.7.x)</td><td><img alt="Maven Central" src="https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-permissions?versionPrefix=0.37"></td>
</tr>
</table>
For stable versions of Compose, we use the latest *stable* version of the Compose compiler. For non-stable versions (alpha, beta, etc), we use the latest compiler at the time of release.
> :warning: **Ensure you are using the Accompanist version that matches with your Compose UI version**: If you upgrade Accompanist, it will upgrade your Compose libraries version via transitive dependencies.
## Libraries
### 📫 [Permissions](./permissions/)
A library that provides [Android runtime permissions][runtimepermissions] support for Jetpack Compose.
### 🖌️ [Drawable Painter](./drawablepainter/)
A library which provides a way to use Android Drawables as Jetpack Compose Painters.
### 📜 [Adaptive](./adaptive/)
A library providing a collection of utilities for adaptive layouts.
### 🧭✨[Navigation-Animation](./navigation-animation/) (Deprecated & Removed)
See our [Migration Guide](https://google.github.io/accompanist/navigation-animation/) for migrating to using built in support for animations in Jetpack Navigation Compose.
### 🧭🎨️ [Navigation-Material](./navigation-material/) (Deprecated & Removed)
See our [Migration Guide](https://google.github.io/accompanist/navigation-material/) for migrating to using built in material-navigation support.
### 🍫 [System UI Controller](./systemuicontroller/) (Deprecated & Removed)
We recommend migrating to edge to edge. See our [Migration Guide](https://google.github.io/accompanist/systemuicontroller/) for more details.
---
## Future?
Any of the features available in this group of libraries may become obsolete in the future, at which point they will (probably) become deprecated.
We will aim to provide a migration path (where possible), to whatever supersedes the functionality.
## Snapshots
Snapshots of the current development version of Accompanist are available, which track the latest commit. See [here](docs/using-snapshot-version.md) for more information.
---
### Why the name?
The library is all about adding some utilities around Compose. Music composing is done by a
composer, and since this library is about supporting composition, the supporting role of an [accompanist](https://en.wikipedia.org/wiki/Accompaniment) felt like a good name.
## Contributions
Please contribute! We will gladly review any pull requests.
Make sure to read the [Contributing](CONTRIBUTING.md) page first though.
## License
```
Copyright 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
[appcompat]: https://developer.android.com/jetpack/androidx/releases/appcompat
[compose]: https://developer.android.com/jetpack/compose
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/
[mdc]: https://github.com/material-components/material-components-android
[windowinsets]: https://developer.android.com/reference/kotlin/android/view/WindowInsets
[viewpager]: https://developer.android.com/reference/kotlin/androidx/viewpager/widget/ViewPager
[runtimepermissions]: https://developer.android.com/guide/topics/permissions/overview
================================================
FILE: adaptive/README.md
================================================
# Adaptive utilities for Jetpack Compose
[](https://search.maven.org/search?q=g:com.google.accompanist)
For more information, visit the documentation: https://google.github.io/accompanist/adaptive
## Download
```groovy
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-adaptive:<version>"
}
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit.
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-adaptive/
================================================
FILE: adaptive/api/current.api
================================================
// Signature format: 4.0
package com.google.accompanist.adaptive {
public final class DisplayFeaturesKt {
method @androidx.compose.runtime.Composable public static java.util.List<androidx.window.layout.DisplayFeature> calculateDisplayFeatures(android.app.Activity activity);
}
public final class FoldAwareColumnKt {
method @androidx.compose.runtime.Composable public static void FoldAwareColumn(java.util.List<? extends androidx.window.layout.DisplayFeature> displayFeatures, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.PaddingValues foldPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super com.google.accompanist.adaptive.FoldAwareColumnScope,kotlin.Unit> content);
}
@androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FoldAwareColumnScope {
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier align(androidx.compose.ui.Modifier, androidx.compose.ui.Alignment.Horizontal alignment);
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, androidx.compose.ui.layout.VerticalAlignmentLine alignmentLine);
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier alignBy(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Measured,java.lang.Integer> alignmentLineBlock);
method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier ignoreFold(androidx.compose.ui.Modifier);
}
@kotlin.jvm.JvmInline public final value class FoldAwareConfiguration {
field public static final com.google.accompanist.adaptive.FoldAwareConfiguration.Companion Companion;
}
public static final class FoldAwareConfiguration.Companion {
method public int getAllFolds();
method public int getHorizontalFoldsOnly();
method public int getVerticalFoldsOnly();
property public final int AllFolds;
property public final int HorizontalFoldsOnly;
property public final int VerticalFoldsOnly;
}
public final class SplitResult {
ctor public SplitResult(androidx.compose.foundation.gestures.Orientation gapOrientation, androidx.compose.ui.geometry.Rect gapBounds);
method public androidx.compose.ui.geometry.Rect getGapBounds();
method public androidx.compose.foundation.gestures.Orientation getGapOrientation();
property public final androidx.compose.ui.geometry.Rect gapBounds;
property public final androidx.compose.foundation.gestures.Orientation gapOrientation;
}
public final class TwoPaneKt {
method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(float splitOffset, optional boolean offsetFromStart, optional float gapWidth);
method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(float splitFraction, optional float gapWidth);
method @androidx.compose.runtime.Composable public static void TwoPane(kotlin.jvm.functions.Function0<kotlin.Unit> first, kotlin.jvm.functions.Function0<kotlin.Unit> second, com.google.accompanist.adaptive.TwoPaneStrategy strategy, java.util.List<? extends androidx.window.layout.DisplayFeature> displayFeatures, optional androidx.compose.ui.Modifier modifier, optional int foldAwareConfiguration);
method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(float splitOffset, optional boolean offsetFromTop, optional float gapHeight);
method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(float splitFraction, optional float gapHeight);
}
public fun interface TwoPaneStrategy {
method public com.google.accompanist.adaptive.SplitResult calculateSplitResult(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates);
}
}
================================================
FILE: adaptive/build.gradle.kts
================================================
/*
* 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("UnstableApiUsage")
plugins {
alias(libs.plugins.accompanist.android.library)
alias(libs.plugins.accompanist.android.library.compose)
alias(libs.plugins.accompanist.android.library.published)
}
android {
namespace = "com.google.accompanist.adaptive"
sourceSets {
named("test") {
java.srcDirs("src/sharedTest/kotlin")
res.srcDirs("src/sharedTest/res")
}
named("androidTest") {
java.srcDirs("src/sharedTest/kotlin")
res.srcDirs("src/sharedTest/res")
}
}
}
dependencies {
api(libs.compose.foundation.foundation)
api(libs.compose.ui.ui)
api(libs.androidx.window)
implementation(libs.kotlin.coroutines.android)
implementation(libs.compose.ui.util)
// ======================
// Test dependencies
// ======================
androidTestImplementation(project(":internal-testutils"))
testImplementation(project(":internal-testutils"))
androidTestImplementation(libs.junit)
testImplementation(libs.junit)
androidTestImplementation(libs.truth)
testImplementation(libs.truth)
androidTestImplementation(libs.compose.ui.test.junit4)
testImplementation(libs.compose.ui.test.junit4)
androidTestImplementation(libs.compose.ui.test.manifest)
testImplementation(libs.compose.ui.test.manifest)
androidTestImplementation(libs.androidx.test.runner)
testImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.window.testing)
testImplementation(libs.androidx.window.testing)
testImplementation(libs.robolectric)
}
================================================
FILE: adaptive/gradle.properties
================================================
POM_ARTIFACT_ID=accompanist-adaptive
POM_NAME=Accompanist Adaptive library
POM_PACKAGING=aar
================================================
FILE: adaptive/src/main/AndroidManifest.xml
================================================
<!--
~ 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.
-->
<manifest />
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/DisplayFeatures.kt
================================================
/*
* 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.google.accompanist.adaptive
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.window.layout.DisplayFeature
import androidx.window.layout.WindowInfoTracker
/**
* Calculates the list of [DisplayFeature]s from the given [activity].
*/
@Composable
public fun calculateDisplayFeatures(activity: Activity): List<DisplayFeature> {
val windowLayoutInfo = remember(activity) {
WindowInfoTracker.getOrCreate(activity).windowLayoutInfo(activity)
}
val displayFeatures by produceState(
initialValue = emptyList<DisplayFeature>(),
key1 = windowLayoutInfo
) {
windowLayoutInfo.collect { info ->
value = info.displayFeatures
}
}
return displayFeatures
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/FoldAwareColumn.kt
================================================
/*
* 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.google.accompanist.adaptive
import android.util.Range
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.LayoutCoordinates
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.layout.ParentDataModifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import kotlin.math.roundToInt
/**
* A simplified version of [Column] that places children in a fold-aware manner.
*
* The layout starts placing children from the top of the available space. If there is a horizontal
* [separating](https://developer.android.com/reference/kotlin/androidx/window/layout/FoldingFeature#isSeparating())
* fold present in the window, then the layout will check to see if any children overlap the fold.
* If a child would overlap the fold in its current position, then the layout will increase its
* y coordinate so that the child is now placed below the fold, and any subsequent children will
* also be placed below the fold.
*
*
* @param displayFeatures a list of display features the device currently has
* @param modifier an optional modifier for the layout
* @param foldPadding the optional padding to add around a fold
* @param horizontalAlignment the horizontal alignment of the layout's children.
*/
@Composable
public fun FoldAwareColumn(
displayFeatures: List<DisplayFeature>,
modifier: Modifier = Modifier,
foldPadding: PaddingValues = PaddingValues(),
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable FoldAwareColumnScope.() -> Unit,
) {
Layout(
modifier = modifier,
measurePolicy = foldAwareColumnMeasurePolicy(
verticalArrangement = Arrangement.Top,
horizontalAlignment = horizontalAlignment,
fold = {
// Extract folding feature if horizontal and separating
displayFeatures.find {
it is FoldingFeature && it.orientation == FoldingFeature.Orientation.HORIZONTAL &&
it.isSeparating
} as FoldingFeature?
},
foldPadding = foldPadding,
),
content = { FoldAwareColumnScopeInstance.content() }
)
}
/**
* FoldAwareColumn version of [rowColumnMeasurePolicy] that uses [FoldAwareColumnMeasurementHelper.foldAwarePlaceHelper]
* method instead of [RowColumnMeasurementHelper.placeHelper]
*/
// TODO: change from internal to private once metalava issue is solved https://issuetracker.google.com/issues/271539608
@Composable
internal fun foldAwareColumnMeasurePolicy(
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
fold: () -> FoldingFeature?,
foldPadding: PaddingValues
) = remember(verticalArrangement, horizontalAlignment, fold, foldPadding) {
val orientation = LayoutOrientation.Vertical
val arrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit =
{ totalSize, size, _, density, outPosition ->
with(verticalArrangement) { density.arrange(totalSize, size, outPosition) }
}
val arrangementSpacing = verticalArrangement.spacing
val crossAxisAlignment = CrossAxisAlignment.horizontal(horizontalAlignment)
val crossAxisSize = SizeMode.Wrap
object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
val placeables = arrayOfNulls<Placeable?>(measurables.size)
val rowColumnMeasureHelper =
FoldAwareColumnMeasurementHelper(
orientation,
arrangement,
arrangementSpacing,
crossAxisSize,
crossAxisAlignment,
measurables,
placeables
)
val measureResult = rowColumnMeasureHelper
.measureWithoutPlacing(
this,
constraints, 0, measurables.size
)
val layoutWidth: Int
val layoutHeight: Int
if (orientation == LayoutOrientation.Horizontal) {
layoutWidth = measureResult.mainAxisSize
layoutHeight = measureResult.crossAxisSize
} else {
layoutWidth = measureResult.crossAxisSize
layoutHeight = measureResult.mainAxisSize
}
// Calculate fold bounds in pixels (including any added fold padding)
val foldBoundsPx = with(density) {
val topPaddingPx = foldPadding.calculateTopPadding().roundToPx()
val bottomPaddingPx = foldPadding.calculateBottomPadding().roundToPx()
fold()?.bounds?.let {
Rect(
left = it.left.toFloat(),
top = it.top.toFloat() - topPaddingPx,
right = it.right.toFloat(),
bottom = it.bottom.toFloat() + bottomPaddingPx
)
}
}
// We only know how much padding is added inside the placement scope, so just add fold height
// and height of the largest child when laying out to cover the maximum possible height
val heightPadding = foldBoundsPx?.let { bounds ->
val largestChildHeight = rowColumnMeasureHelper.placeables.maxOfOrNull {
if ((it?.parentData as? RowColumnParentData)?.ignoreFold == true) {
0
} else {
it?.height ?: 0
}
} ?: 0
bounds.height.roundToInt() + largestChildHeight
} ?: 0
val paddedLayoutHeight = layoutHeight + heightPadding
return layout(layoutWidth, paddedLayoutHeight) {
rowColumnMeasureHelper.foldAwarePlaceHelper(
this,
measureResult,
0,
layoutDirection,
foldBoundsPx
)
}
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
) = MinIntrinsicWidthMeasureBlock(orientation)(
measurables,
height,
arrangementSpacing.roundToPx()
)
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
) = MinIntrinsicHeightMeasureBlock(orientation)(
measurables,
width,
arrangementSpacing.roundToPx()
)
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
) = MaxIntrinsicWidthMeasureBlock(orientation)(
measurables,
height,
arrangementSpacing.roundToPx()
)
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
) = MaxIntrinsicHeightMeasureBlock(orientation)(
measurables,
width,
arrangementSpacing.roundToPx()
)
}
}
/**
* Inherits from [RowColumnMeasurementHelper] to place children in a fold-aware manner
*/
private class FoldAwareColumnMeasurementHelper(
orientation: LayoutOrientation,
arrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit,
arrangementSpacing: Dp,
crossAxisSize: SizeMode,
crossAxisAlignment: CrossAxisAlignment,
measurables: List<Measurable>,
placeables: Array<Placeable?>
) : RowColumnMeasurementHelper(
orientation,
arrangement,
arrangementSpacing,
crossAxisSize,
crossAxisAlignment,
measurables,
placeables
) {
/**
* Copy of [placeHelper] that has been modified for FoldAwareColumn implementation
*/
@OptIn(ExperimentalComposeUiApi::class)
fun foldAwarePlaceHelper(
placeableScope: Placeable.PlacementScope,
measureResult: RowColumnMeasureHelperResult,
crossAxisOffset: Int,
layoutDirection: LayoutDirection,
foldBoundsPx: Rect?
) {
with(placeableScope) {
val layoutBounds = coordinates!!.trueBoundsInWindow()
var placeableY = 0
for (i in measureResult.startIndex until measureResult.endIndex) {
val placeable = placeables[i]!!
val mainAxisPositions = measureResult.mainAxisPositions
val crossAxisPosition = getCrossAxisPosition(
placeable,
(measurables[i].parentData as? RowColumnParentData),
measureResult.crossAxisSize,
layoutDirection,
measureResult.beforeCrossAxisAlignmentLine
) + crossAxisOffset
if (orientation == LayoutOrientation.Horizontal) {
placeable.place(
mainAxisPositions[i - measureResult.startIndex],
crossAxisPosition
)
} else {
val relativeBounds = Rect(
left = 0f,
top = placeableY.toFloat(),
right = placeable.width.toFloat(),
bottom = (placeableY + placeable.height).toFloat()
)
val absoluteBounds =
relativeBounds.translate(layoutBounds.left, layoutBounds.top)
// If placeable overlaps fold, push placeable below
if (foldBoundsPx?.overlapsVertically(absoluteBounds) == true &&
(placeable.parentData as? RowColumnParentData)?.ignoreFold != true
) {
placeableY = (foldBoundsPx.bottom - layoutBounds.top).toInt()
}
placeable.place(crossAxisPosition, placeableY)
placeableY += placeable.height
}
}
}
}
}
/**
* Copy of original [LayoutCoordinates.boundsInWindow], but without the nonzero dimension check.
*
* Instead of returning [Rect.Zero] for a layout with zero width/height, this method will still
* return a Rect with the layout's bounds.
*/
@VisibleForTesting
internal fun LayoutCoordinates.trueBoundsInWindow(): Rect {
val root = findRootCoordinates()
val bounds = boundsInRoot()
val rootWidth = root.size.width.toFloat()
val rootHeight = root.size.height.toFloat()
val boundsLeft = bounds.left.coerceIn(0f, rootWidth)
val boundsTop = bounds.top.coerceIn(0f, rootHeight)
val boundsRight = bounds.right.coerceIn(0f, rootWidth)
val boundsBottom = bounds.bottom.coerceIn(0f, rootHeight)
val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
val topRight = root.localToWindow(Offset(boundsRight, boundsTop))
val bottomRight = root.localToWindow(Offset(boundsRight, boundsBottom))
val bottomLeft = root.localToWindow(Offset(boundsLeft, boundsBottom))
val left = minOf(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)
val top = minOf(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)
val right = maxOf(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x)
val bottom = maxOf(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y)
return Rect(left, top, right, bottom)
}
/**
* Checks if the vertical ranges of the two Rects overlap (inclusive)
*/
private fun Rect.overlapsVertically(other: Rect): Boolean {
val verticalRange = Range(top, bottom)
val otherVerticalRange = Range(other.top, other.bottom)
return verticalRange.overlaps(otherVerticalRange)
}
/**
* Inclusive check to see if the given float ranges overlap
*/
private fun Range<Float>.overlaps(other: Range<Float>): Boolean {
return (lower >= other.lower && lower <= other.upper) || (upper >= other.lower && upper <= other.upper)
}
/**
* Copy of [RowColumnParentData] that has been modified to include the new ignoreFold field.
*/
internal data class RowColumnParentData(
var weight: Float = 0f,
var fill: Boolean = true,
var crossAxisAlignment: CrossAxisAlignment? = null,
var ignoreFold: Boolean = false
)
internal class IgnoreFoldModifier(
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?) =
((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.ignoreFold = true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
return other is IgnoreFoldModifier
}
override fun hashCode(): Int {
return 0
}
override fun toString(): String =
"IgnoreFoldModifier(ignoreFold=true)"
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/FoldAwareColumnScope.kt
================================================
/*
* 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.google.accompanist.adaptive
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Measured
import androidx.compose.ui.layout.VerticalAlignmentLine
import androidx.compose.ui.platform.debugInspectorInfo
/**
* Copy of [ColumnScope] that excludes the weight Modifier attribute.
*
* Also adds a new [ignoreFold] Modifier attribute.
*/
@LayoutScopeMarker
@Immutable
public interface FoldAwareColumnScope {
/**
* Ignore the fold when placing this child within the [FoldAwareColumn].
*/
@Stable
public fun Modifier.ignoreFold(): Modifier
/**
* Align the element horizontally within the [Column]. This alignment will have priority over
* the [Column]'s `horizontalAlignment` parameter.
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.SimpleAlignInColumn
*/
@Stable
public fun Modifier.align(alignment: Alignment.Horizontal): Modifier
/**
* Position the element horizontally such that its [alignmentLine] aligns with sibling elements
* also configured to [alignBy]. [alignBy] is a form of [align],
* so both modifiers will not work together if specified for the same layout.
* Within a [Column], all components with [alignBy] will align horizontally using
* the specified [VerticalAlignmentLine]s or values provided using the other
* [alignBy] overload, forming a sibling group.
* At least one element of the sibling group will be placed as it had [Alignment.Start] align
* in [Column], and the alignment of the other siblings will be then determined such that
* the alignment lines coincide. Note that if only one element in a [Column] has the
* [alignBy] modifier specified the element will be positioned
* as if it had [Alignment.Start] align.
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.SimpleRelativeToSiblingsInColumn
*/
@Stable
public fun Modifier.alignBy(alignmentLine: VerticalAlignmentLine): Modifier
/**
* Position the element horizontally such that the alignment line for the content as
* determined by [alignmentLineBlock] aligns with sibling elements also configured to
* [alignBy]. [alignBy] is a form of [align], so both modifiers
* will not work together if specified for the same layout.
* Within a [Column], all components with [alignBy] will align horizontally using
* the specified [VerticalAlignmentLine]s or values obtained from [alignmentLineBlock],
* forming a sibling group.
* At least one element of the sibling group will be placed as it had [Alignment.Start] align
* in [Column], and the alignment of the other siblings will be then determined such that
* the alignment lines coincide. Note that if only one element in a [Column] has the
* [alignBy] modifier specified the element will be positioned
* as if it had [Alignment.Start] align.
*
* Example usage:
* @sample androidx.compose.foundation.layout.samples.SimpleRelativeToSiblings
*/
@Stable
public fun Modifier.alignBy(alignmentLineBlock: (Measured) -> Int): Modifier
}
internal object FoldAwareColumnScopeInstance : FoldAwareColumnScope {
@Stable
override fun Modifier.ignoreFold() = this.then(
IgnoreFoldModifier(
inspectorInfo = debugInspectorInfo {
name = "ignoreFold"
value = true
}
)
)
@Stable
override fun Modifier.align(alignment: Alignment.Horizontal) = this.then(
HorizontalAlignModifier(
horizontal = alignment,
inspectorInfo = debugInspectorInfo {
name = "align"
value = alignment
}
)
)
@Stable
override fun Modifier.alignBy(alignmentLine: VerticalAlignmentLine) = this.then(
SiblingsAlignedModifier.WithAlignmentLine(
alignmentLine = alignmentLine,
inspectorInfo = debugInspectorInfo {
name = "alignBy"
value = alignmentLine
}
)
)
@Stable
override fun Modifier.alignBy(alignmentLineBlock: (Measured) -> Int) = this.then(
SiblingsAlignedModifier.WithAlignmentLineBlock(
block = alignmentLineBlock,
inspectorInfo = debugInspectorInfo {
name = "alignBy"
value = alignmentLineBlock
}
)
)
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/RowColumnImpl.kt
================================================
/*
* 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.google.accompanist.adaptive
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.Measured
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastForEach
import com.google.accompanist.adaptive.LayoutOrientation.Horizontal
import com.google.accompanist.adaptive.LayoutOrientation.Vertical
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
/**
* Copied from:
* RowColumnImpl.kt
* https://android-review.googlesource.com/c/platform/frameworks/support/+/2260390/27/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
*
* The only changes were updating access modifiers and removing unused code
*/
/**
* [Row] will be [Horizontal], [Column] is [Vertical].
*/
internal enum class LayoutOrientation {
Horizontal,
Vertical
}
/**
* Used to specify the alignment of a layout's children, in cross axis direction.
*/
@Immutable
internal sealed class CrossAxisAlignment {
/**
* Aligns to [size]. If this is a vertical alignment, [layoutDirection] should be
* [LayoutDirection.Ltr].
*
* @param size The remaining space (total size - content size) in the container.
* @param layoutDirection The layout direction of the content if horizontal or
* [LayoutDirection.Ltr] if vertical.
* @param placeable The item being aligned.
* @param beforeCrossAxisAlignmentLine The space before the cross-axis alignment line if
* an alignment line is being used or 0 if no alignment line is being used.
*/
internal abstract fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int
/**
* Returns `true` if this is [Relative].
*/
internal open val isRelative: Boolean
get() = false
/**
* Returns the alignment line position relative to the left/top of the space or `null` if
* this alignment doesn't rely on alignment lines.
*/
internal open fun calculateAlignmentLinePosition(placeable: Placeable): Int? = null
companion object {
/**
* Place children such that their center is in the middle of the cross axis.
*/
@Stable
val Center: CrossAxisAlignment = CenterCrossAxisAlignment
/**
* Place children such that their start edge is aligned to the start edge of the cross
* axis. TODO(popam): Consider rtl directionality.
*/
@Stable
val Start: CrossAxisAlignment = StartCrossAxisAlignment
/**
* Place children such that their end edge is aligned to the end edge of the cross
* axis. TODO(popam): Consider rtl directionality.
*/
@Stable
val End: CrossAxisAlignment = EndCrossAxisAlignment
/**
* Align children by their baseline.
*/
fun AlignmentLine(alignmentLine: AlignmentLine): CrossAxisAlignment =
AlignmentLineCrossAxisAlignment(AlignmentLineProvider.Value(alignmentLine))
/**
* Align children relative to their siblings using the alignment line provided as a
* parameter using [AlignmentLineProvider].
*/
internal fun Relative(alignmentLineProvider: AlignmentLineProvider): CrossAxisAlignment =
AlignmentLineCrossAxisAlignment(alignmentLineProvider)
/**
* Align children with vertical alignment.
*/
internal fun vertical(vertical: Alignment.Vertical): CrossAxisAlignment =
VerticalCrossAxisAlignment(vertical)
/**
* Align children with horizontal alignment.
*/
internal fun horizontal(horizontal: Alignment.Horizontal): CrossAxisAlignment =
HorizontalCrossAxisAlignment(horizontal)
}
private object CenterCrossAxisAlignment : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return size / 2
}
}
private object StartCrossAxisAlignment : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return if (layoutDirection == LayoutDirection.Ltr) 0 else size
}
}
private object EndCrossAxisAlignment : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return if (layoutDirection == LayoutDirection.Ltr) size else 0
}
}
private class AlignmentLineCrossAxisAlignment(
val alignmentLineProvider: AlignmentLineProvider
) : CrossAxisAlignment() {
override val isRelative: Boolean
get() = true
override fun calculateAlignmentLinePosition(placeable: Placeable): Int {
return alignmentLineProvider.calculateAlignmentLinePosition(placeable)
}
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
val alignmentLinePosition =
alignmentLineProvider.calculateAlignmentLinePosition(placeable)
return if (alignmentLinePosition != AlignmentLine.Unspecified) {
val line = beforeCrossAxisAlignmentLine - alignmentLinePosition
if (layoutDirection == LayoutDirection.Rtl) {
size - line
} else {
line
}
} else {
0
}
}
}
private class VerticalCrossAxisAlignment(
val vertical: Alignment.Vertical
) : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return vertical.align(0, size)
}
}
private class HorizontalCrossAxisAlignment(
val horizontal: Alignment.Horizontal
) : CrossAxisAlignment() {
override fun align(
size: Int,
layoutDirection: LayoutDirection,
placeable: Placeable,
beforeCrossAxisAlignmentLine: Int
): Int {
return horizontal.align(0, size, layoutDirection)
}
}
}
/**
* Box [Constraints], but which abstract away width and height in favor of main axis and cross axis.
*/
internal data class OrientationIndependentConstraints(
val mainAxisMin: Int,
val mainAxisMax: Int,
val crossAxisMin: Int,
val crossAxisMax: Int
) {
constructor(c: Constraints, orientation: LayoutOrientation) : this(
if (orientation === Horizontal) c.minWidth else c.minHeight,
if (orientation === Horizontal) c.maxWidth else c.maxHeight,
if (orientation === Horizontal) c.minHeight else c.minWidth,
if (orientation === Horizontal) c.maxHeight else c.maxWidth
)
// Creates a new instance with the same main axis constraints and maximum tight cross axis.
fun stretchCrossAxis() = OrientationIndependentConstraints(
mainAxisMin,
mainAxisMax,
if (crossAxisMax != Constraints.Infinity) crossAxisMax else crossAxisMin,
crossAxisMax
)
// Given an orientation, resolves the current instance to traditional constraints.
fun toBoxConstraints(orientation: LayoutOrientation) =
if (orientation === Horizontal) {
Constraints(mainAxisMin, mainAxisMax, crossAxisMin, crossAxisMax)
} else {
Constraints(crossAxisMin, crossAxisMax, mainAxisMin, mainAxisMax)
}
// Given an orientation, resolves the max width constraint this instance represents.
fun maxWidth(orientation: LayoutOrientation) =
if (orientation === Horizontal) {
mainAxisMax
} else {
crossAxisMax
}
// Given an orientation, resolves the max height constraint this instance represents.
fun maxHeight(orientation: LayoutOrientation) =
if (orientation === Horizontal) {
crossAxisMax
} else {
mainAxisMax
}
}
internal val IntrinsicMeasurable.rowColumnParentData: RowColumnParentData?
get() = parentData as? RowColumnParentData
internal val RowColumnParentData?.weight: Float
get() = this?.weight ?: 0f
internal val RowColumnParentData?.fill: Boolean
get() = this?.fill ?: true
internal val RowColumnParentData?.crossAxisAlignment: CrossAxisAlignment?
get() = this?.crossAxisAlignment
internal val RowColumnParentData?.isRelative: Boolean
get() = this.crossAxisAlignment?.isRelative ?: false
internal fun MinIntrinsicWidthMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMinWidth
} else {
IntrinsicMeasureBlocks.VerticalMinWidth
}
internal fun MinIntrinsicHeightMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMinHeight
} else {
IntrinsicMeasureBlocks.VerticalMinHeight
}
internal fun MaxIntrinsicWidthMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMaxWidth
} else {
IntrinsicMeasureBlocks.VerticalMaxWidth
}
internal fun MaxIntrinsicHeightMeasureBlock(orientation: LayoutOrientation) =
if (orientation == Horizontal) {
IntrinsicMeasureBlocks.HorizontalMaxHeight
} else {
IntrinsicMeasureBlocks.VerticalMaxHeight
}
internal object IntrinsicMeasureBlocks {
val HorizontalMinWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Horizontal,
Horizontal
)
}
val VerticalMinWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Vertical,
Horizontal
)
}
val HorizontalMinHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Horizontal,
Vertical
)
}
val VerticalMinHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Vertical,
Vertical
)
}
val HorizontalMaxWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> maxIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Horizontal,
Horizontal
)
}
val VerticalMaxWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> maxIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
mainAxisSpacing,
Vertical,
Horizontal
)
}
val HorizontalMaxHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> maxIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Horizontal,
Vertical
)
}
val VerticalMaxHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
{ measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> maxIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
mainAxisSpacing,
Vertical,
Vertical
)
}
}
private fun intrinsicSize(
children: List<IntrinsicMeasurable>,
intrinsicMainSize: IntrinsicMeasurable.(Int) -> Int,
intrinsicCrossSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisAvailable: Int,
mainAxisSpacing: Int,
layoutOrientation: LayoutOrientation,
intrinsicOrientation: LayoutOrientation
) = if (layoutOrientation == intrinsicOrientation) {
intrinsicMainAxisSize(children, intrinsicMainSize, crossAxisAvailable, mainAxisSpacing)
} else {
intrinsicCrossAxisSize(
children,
intrinsicCrossSize,
intrinsicMainSize,
crossAxisAvailable,
mainAxisSpacing
)
}
private fun intrinsicMainAxisSize(
children: List<IntrinsicMeasurable>,
mainAxisSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisAvailable: Int,
mainAxisSpacing: Int
): Int {
var weightUnitSpace = 0
var fixedSpace = 0
var totalWeight = 0f
children.fastForEach { child ->
val weight = child.rowColumnParentData.weight
val size = child.mainAxisSize(crossAxisAvailable)
if (weight == 0f) {
fixedSpace += size
} else if (weight > 0f) {
totalWeight += weight
weightUnitSpace = max(weightUnitSpace, (size / weight).roundToInt())
}
}
return (weightUnitSpace * totalWeight).roundToInt() + fixedSpace +
(children.size - 1) * mainAxisSpacing
}
private fun intrinsicCrossAxisSize(
children: List<IntrinsicMeasurable>,
mainAxisSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisSize: IntrinsicMeasurable.(Int) -> Int,
mainAxisAvailable: Int,
mainAxisSpacing: Int
): Int {
var fixedSpace = min((children.size - 1) * mainAxisSpacing, mainAxisAvailable)
var crossAxisMax = 0
var totalWeight = 0f
children.fastForEach { child ->
val weight = child.rowColumnParentData.weight
if (weight == 0f) {
// Ask the child how much main axis space it wants to occupy. This cannot be more
// than the remaining available space.
val mainAxisSpace = min(
child.mainAxisSize(Constraints.Infinity),
mainAxisAvailable - fixedSpace
)
fixedSpace += mainAxisSpace
// Now that the assigned main axis space is known, ask about the cross axis space.
crossAxisMax = max(crossAxisMax, child.crossAxisSize(mainAxisSpace))
} else if (weight > 0f) {
totalWeight += weight
}
}
// For weighted children, calculate how much main axis space weight=1 would represent.
val weightUnitSpace = if (totalWeight == 0f) {
0
} else if (mainAxisAvailable == Constraints.Infinity) {
Constraints.Infinity
} else {
(max(mainAxisAvailable - fixedSpace, 0) / totalWeight).roundToInt()
}
children.fastForEach { child ->
val weight = child.rowColumnParentData.weight
// Now the main axis for weighted children is known, so ask about the cross axis space.
if (weight > 0f) {
crossAxisMax = max(
crossAxisMax,
child.crossAxisSize(
if (weightUnitSpace != Constraints.Infinity) {
(weightUnitSpace * weight).roundToInt()
} else {
Constraints.Infinity
}
)
)
}
}
return crossAxisMax
}
internal class LayoutWeightImpl(
val weight: Float,
val fill: Boolean,
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?) =
((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.weight = weight
it.fill = fill
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? LayoutWeightImpl ?: return false
return weight == otherModifier.weight &&
fill == otherModifier.fill
}
override fun hashCode(): Int {
var result = weight.hashCode()
result = 31 * result + fill.hashCode()
return result
}
override fun toString(): String =
"LayoutWeightImpl(weight=$weight, fill=$fill)"
}
internal sealed class SiblingsAlignedModifier(
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
abstract override fun Density.modifyParentData(parentData: Any?): Any?
internal class WithAlignmentLineBlock(
val block: (Measured) -> Int,
inspectorInfo: InspectorInfo.() -> Unit
) : SiblingsAlignedModifier(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?): Any {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment =
CrossAxisAlignment.Relative(AlignmentLineProvider.Block(block))
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? WithAlignmentLineBlock ?: return false
return block == otherModifier.block
}
override fun hashCode(): Int = block.hashCode()
override fun toString(): String = "WithAlignmentLineBlock(block=$block)"
}
internal class WithAlignmentLine(
val alignmentLine: AlignmentLine,
inspectorInfo: InspectorInfo.() -> Unit
) : SiblingsAlignedModifier(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?): Any {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment =
CrossAxisAlignment.Relative(AlignmentLineProvider.Value(alignmentLine))
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? WithAlignmentLine ?: return false
return alignmentLine == otherModifier.alignmentLine
}
override fun hashCode(): Int = alignmentLine.hashCode()
override fun toString(): String = "WithAlignmentLine(line=$alignmentLine)"
}
}
internal class HorizontalAlignModifier(
val horizontal: Alignment.Horizontal,
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?): RowColumnParentData {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment = CrossAxisAlignment.horizontal(horizontal)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? HorizontalAlignModifier ?: return false
return horizontal == otherModifier.horizontal
}
override fun hashCode(): Int = horizontal.hashCode()
override fun toString(): String =
"HorizontalAlignModifier(horizontal=$horizontal)"
}
internal class VerticalAlignModifier(
val vertical: Alignment.Vertical,
inspectorInfo: InspectorInfo.() -> Unit
) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
override fun Density.modifyParentData(parentData: Any?): RowColumnParentData {
return ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
it.crossAxisAlignment = CrossAxisAlignment.vertical(vertical)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? VerticalAlignModifier ?: return false
return vertical == otherModifier.vertical
}
override fun hashCode(): Int = vertical.hashCode()
override fun toString(): String =
"VerticalAlignModifier(vertical=$vertical)"
}
/**
* Provides the alignment line.
*/
internal sealed class AlignmentLineProvider {
abstract fun calculateAlignmentLinePosition(placeable: Placeable): Int
data class Block(val lineProviderBlock: (Measured) -> Int) : AlignmentLineProvider() {
override fun calculateAlignmentLinePosition(
placeable: Placeable
): Int {
return lineProviderBlock(placeable)
}
}
data class Value(val alignmentLine: AlignmentLine) : AlignmentLineProvider() {
override fun calculateAlignmentLinePosition(placeable: Placeable): Int {
return placeable[alignmentLine]
}
}
}
/**
* Used to specify how a layout chooses its own size when multiple behaviors are possible.
*/
// TODO(popam): remove this when Flow is reworked
internal enum class SizeMode {
/**
* Minimize the amount of free space by wrapping the children,
* subject to the incoming layout constraints.
*/
Wrap,
/**
* Maximize the amount of free space by expanding to fill the available space,
* subject to the incoming layout constraints.
*/
Expand
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/RowColumnMeasurementHelper.kt
================================================
/*
* 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.google.accompanist.adaptive
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sign
/**
* Copied from:
* RowColumnMeasurementHelper.kt
* https://android-review.googlesource.com/c/platform/frameworks/support/+/2260390/27/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurementHelper.kt
*
* The only changes were updating access modifiers and making RowColumnMeasurementHelper an open class
*/
/**
* This is a data class that holds the determined width, height of a row,
* and information on how to retrieve main axis and cross axis positions.
*/
internal class RowColumnMeasureHelperResult(
val crossAxisSize: Int,
val mainAxisSize: Int,
val startIndex: Int,
val endIndex: Int,
val beforeCrossAxisAlignmentLine: Int,
val mainAxisPositions: IntArray,
)
/**
* RowColumnMeasurementHelper
* Measures the row and column without placing, useful for reusing row/column logic
*/
internal open class RowColumnMeasurementHelper(
val orientation: LayoutOrientation,
val arrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit,
val arrangementSpacing: Dp,
val crossAxisSize: SizeMode,
val crossAxisAlignment: CrossAxisAlignment,
val measurables: List<Measurable>,
val placeables: Array<Placeable?>
) {
private val rowColumnParentData = Array(measurables.size) {
measurables[it].rowColumnParentData
}
fun Placeable.mainAxisSize() =
if (orientation == LayoutOrientation.Horizontal) width else height
fun Placeable.crossAxisSize() =
if (orientation == LayoutOrientation.Horizontal) height else width
/**
* Measures the row and column without placing, useful for reusing row/column logic
*
* @param measureScope The measure scope to retrieve density
* @param constraints The desired constraints for the startIndex and endIndex
* can hold null items if not measured.
* @param startIndex The startIndex (inclusive) when examining measurables, placeable
* and parentData
* @param endIndex The ending index (exclusive) when examinning measurable, placeable
* and parentData
*/
fun measureWithoutPlacing(
measureScope: MeasureScope,
constraints: Constraints,
startIndex: Int,
endIndex: Int
): RowColumnMeasureHelperResult {
@Suppress("NAME_SHADOWING")
val constraints = OrientationIndependentConstraints(constraints, orientation)
val arrangementSpacingPx = with(measureScope) {
arrangementSpacing.roundToPx()
}
var totalWeight = 0f
var fixedSpace = 0
var crossAxisSpace = 0
var weightChildrenCount = 0
var anyAlignBy = false
val subSize = endIndex - startIndex
// First measure children with zero weight.
var spaceAfterLastNoWeight = 0
for (i in startIndex until endIndex) {
val child = measurables[i]
val parentData = rowColumnParentData[i]
val weight = parentData.weight
if (weight > 0f) {
totalWeight += weight
++weightChildrenCount
} else {
val mainAxisMax = constraints.mainAxisMax
val placeable = placeables[i] ?: child.measure(
// Ask for preferred main axis size.
constraints.copy(
mainAxisMin = 0,
mainAxisMax = if (mainAxisMax == Constraints.Infinity) {
Constraints.Infinity
} else {
mainAxisMax - fixedSpace
},
crossAxisMin = 0
).toBoxConstraints(orientation)
)
spaceAfterLastNoWeight = min(
arrangementSpacingPx,
mainAxisMax - fixedSpace - placeable.mainAxisSize()
)
fixedSpace += placeable.mainAxisSize() + spaceAfterLastNoWeight
crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
anyAlignBy = anyAlignBy || parentData.isRelative
placeables[i] = placeable
}
}
var weightedSpace = 0
if (weightChildrenCount == 0) {
// fixedSpace contains an extra spacing after the last non-weight child.
fixedSpace -= spaceAfterLastNoWeight
} else {
// Measure the rest according to their weights in the remaining main axis space.
val targetSpace =
if (totalWeight > 0f && constraints.mainAxisMax != Constraints.Infinity) {
constraints.mainAxisMax
} else {
constraints.mainAxisMin
}
val remainingToTarget =
targetSpace - fixedSpace - arrangementSpacingPx * (weightChildrenCount - 1)
val weightUnitSpace = if (totalWeight > 0) remainingToTarget / totalWeight else 0f
var remainder = remainingToTarget - (startIndex until endIndex).sumOf {
(weightUnitSpace * rowColumnParentData[it].weight).roundToInt()
}
for (i in startIndex until endIndex) {
if (placeables[i] == null) {
val child = measurables[i]
val parentData = rowColumnParentData[i]
val weight = parentData.weight
check(weight > 0) { "All weights <= 0 should have placeables" }
// After the weightUnitSpace rounding, the total space going to be occupied
// can be smaller or larger than remainingToTarget. Here we distribute the
// loss or gain remainder evenly to the first children.
val remainderUnit = remainder.sign
remainder -= remainderUnit
val childMainAxisSize = max(
0,
(weightUnitSpace * weight).roundToInt() + remainderUnit
)
val placeable = child.measure(
OrientationIndependentConstraints(
if (parentData.fill && childMainAxisSize != Constraints.Infinity) {
childMainAxisSize
} else {
0
},
childMainAxisSize,
0,
constraints.crossAxisMax
).toBoxConstraints(orientation)
)
weightedSpace += placeable.mainAxisSize()
crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
anyAlignBy = anyAlignBy || parentData.isRelative
placeables[i] = placeable
}
}
weightedSpace = (weightedSpace + arrangementSpacingPx * (weightChildrenCount - 1))
.coerceAtMost(constraints.mainAxisMax - fixedSpace)
}
var beforeCrossAxisAlignmentLine = 0
var afterCrossAxisAlignmentLine = 0
if (anyAlignBy) {
for (i in startIndex until endIndex) {
val placeable = placeables[i]!!
val parentData = rowColumnParentData[i]
val alignmentLinePosition = parentData.crossAxisAlignment
?.calculateAlignmentLinePosition(placeable)
if (alignmentLinePosition != null) {
beforeCrossAxisAlignmentLine = max(
beforeCrossAxisAlignmentLine,
alignmentLinePosition.let {
if (it != AlignmentLine.Unspecified) it else 0
}
)
afterCrossAxisAlignmentLine = max(
afterCrossAxisAlignmentLine,
placeable.crossAxisSize() -
(
alignmentLinePosition.let {
if (it != AlignmentLine.Unspecified) {
it
} else {
placeable.crossAxisSize()
}
}
)
)
}
}
}
// Compute the Row or Column size and position the children.
val mainAxisLayoutSize = max(fixedSpace + weightedSpace, constraints.mainAxisMin)
val crossAxisLayoutSize = if (constraints.crossAxisMax != Constraints.Infinity &&
crossAxisSize == SizeMode.Expand
) {
constraints.crossAxisMax
} else {
max(
crossAxisSpace,
max(
constraints.crossAxisMin,
beforeCrossAxisAlignmentLine + afterCrossAxisAlignmentLine
)
)
}
val mainAxisPositions = IntArray(subSize) { 0 }
val childrenMainAxisSize = IntArray(subSize) { index ->
placeables[index + startIndex]!!.mainAxisSize()
}
return RowColumnMeasureHelperResult(
mainAxisSize = mainAxisLayoutSize,
crossAxisSize = crossAxisLayoutSize,
startIndex = startIndex,
endIndex = endIndex,
beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine,
mainAxisPositions = mainAxisPositions(
mainAxisLayoutSize,
childrenMainAxisSize,
mainAxisPositions,
measureScope
)
)
}
private fun mainAxisPositions(
mainAxisLayoutSize: Int,
childrenMainAxisSize: IntArray,
mainAxisPositions: IntArray,
measureScope: MeasureScope
): IntArray {
arrangement(
mainAxisLayoutSize,
childrenMainAxisSize,
measureScope.layoutDirection,
measureScope,
mainAxisPositions
)
return mainAxisPositions
}
protected fun getCrossAxisPosition(
placeable: Placeable,
parentData: RowColumnParentData?,
crossAxisLayoutSize: Int,
layoutDirection: LayoutDirection,
beforeCrossAxisAlignmentLine: Int
): Int {
val childCrossAlignment = parentData?.crossAxisAlignment ?: crossAxisAlignment
return childCrossAlignment.align(
size = crossAxisLayoutSize - placeable.crossAxisSize(),
layoutDirection = if (orientation == LayoutOrientation.Horizontal) {
LayoutDirection.Ltr
} else {
layoutDirection
},
placeable = placeable,
beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine
)
}
fun placeHelper(
placeableScope: Placeable.PlacementScope,
measureResult: RowColumnMeasureHelperResult,
crossAxisOffset: Int,
layoutDirection: LayoutDirection,
) {
with(placeableScope) {
for (i in measureResult.startIndex until measureResult.endIndex) {
val placeable = placeables[i]
placeable!!
val mainAxisPositions = measureResult.mainAxisPositions
val crossAxisPosition = getCrossAxisPosition(
placeable,
(measurables[i].parentData as? RowColumnParentData),
measureResult.crossAxisSize,
layoutDirection,
measureResult.beforeCrossAxisAlignmentLine
) + crossAxisOffset
if (orientation == LayoutOrientation.Horizontal) {
placeable.place(
mainAxisPositions[i - measureResult.startIndex],
crossAxisPosition
)
} else {
placeable.place(
crossAxisPosition,
mainAxisPositions[i - measureResult.startIndex]
)
}
}
}
}
}
================================================
FILE: adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt
================================================
/*
* 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.google.accompanist.adaptive
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
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.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import kotlin.math.roundToInt
/**
* A layout that places two different pieces of content defined by the [first] and [second]
* slots where the arrangement, sizes and separation behaviour is controlled by [TwoPaneStrategy].
*
* [TwoPane] is fold and hinges aware using the provided the [displayFeatures] (which should
* normally be calculated via [calculateDisplayFeatures]). The layout will be adapted to properly
* separate [first] and [second] panes so they don't interfere with hardware hinges (vertical or
* horizontal) as specified in [displayFeatures], or respect folds when needed (for example, when
* foldable is half-folded (90-degree fold AKA tabletop) the split will become on the bend).
*
* To only be aware of folds with a specific orientation, pass in an alternate
* [foldAwareConfiguration] to only adjust for vertical or horizontal folds.
*
* The [TwoPane] layout will always place both [first] and [second], based on the provided
* [strategy] and window environment. If you instead only want to place one or the other,
* that should be controlled at a higher level and not calling [TwoPane] if placing both is not
* desired.
*
* @param first the first content of the layout, a left-most in LTR, a right-most in RTL and
* top-most in a vertical split based on the [SplitResult] of [TwoPaneStrategy.calculateSplitResult]
* @param second the second content of the layout, a right-most in the LTR, a left-most in the RTL
* and the bottom-most in a vertical split based on the [SplitResult] of
* [TwoPaneStrategy.calculateSplitResult]
* @param strategy strategy of the two pane that controls the arrangement of the layout
* @param displayFeatures the list of known display features to automatically avoid
* @param foldAwareConfiguration the types of display features to automatically avoid
* @param modifier an optional modifier for the layout
*/
@Composable
public fun TwoPane(
first: @Composable () -> Unit,
second: @Composable () -> Unit,
strategy: TwoPaneStrategy,
displayFeatures: List<DisplayFeature>,
modifier: Modifier = Modifier,
foldAwareConfiguration: FoldAwareConfiguration = FoldAwareConfiguration.AllFolds,
) {
TwoPane(
first = first,
second = second,
strategy = when (foldAwareConfiguration) {
FoldAwareConfiguration.HorizontalFoldsOnly -> {
VerticalTwoPaneStrategy(
displayFeatures = displayFeatures,
defaultStrategy = strategy,
)
}
FoldAwareConfiguration.VerticalFoldsOnly -> {
HorizontalTwoPaneStrategy(
displayFeatures = displayFeatures,
defaultStrategy = strategy,
)
}
FoldAwareConfiguration.AllFolds -> {
TwoPaneStrategy(
displayFeatures = displayFeatures,
defaultStrategy = strategy,
)
}
else -> error("Unknown FoldAware value!")
},
modifier = modifier,
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
internal fun TwoPane(
first: @Composable () -> Unit,
second: @Composable () -> Unit,
strategy: TwoPaneStrategy,
modifier: Modifier = Modifier
) {
val density = LocalDensity.current
Layout(
modifier = modifier.wrapContentSize(),
content = {
Box(Modifier.layoutId("first")) {
first()
}
Box(Modifier.layoutId("second")) {
second()
}
}
) { measurable, constraints ->
val firstMeasurable = measurable.find { it.layoutId == "first" }!!
val secondMeasurable = measurable.find { it.layoutId == "second" }!!
layout(constraints.maxWidth, constraints.maxHeight) {
val splitResult = strategy.calculateSplitResult(
density = density,
layoutDirection = layoutDirection,
layoutCoordinates = coordinates ?: return@layout
)
val gapOrientation = splitResult.gapOrientation
val gapBounds = splitResult.gapBounds
val gapLeft = constraints.constrainWidth(gapBounds.left.roundToInt())
val gapRight = constraints.constrainWidth(gapBounds.right.roundToInt())
val gapTop = constraints.constrainHeight(gapBounds.top.roundToInt())
val gapBottom = constraints.constrainHeight(gapBounds.bottom.roundToInt())
val firstConstraints =
if (gapOrientation == Orientation.Vertical) {
val width = when (layoutDirection) {
LayoutDirection.Ltr -> gapLeft
LayoutDirection.Rtl -> constraints.maxWidth - gapRight
}
constraints.copy(minWidth = width, maxWidth = width)
} else {
constraints.copy(minHeight = gapTop, maxHeight = gapTop)
}
val secondConstraints =
if (gapOrientation == Orientation.Vertical) {
val width = when (layoutDirection) {
LayoutDirection.Ltr -> constraints.maxWidth - gapRight
LayoutDirection.Rtl -> gapLeft
}
constraints.copy(minWidth = width, maxWidth = width)
} else {
val height = constraints.maxHeight - gapBottom
constraints.copy(minHeight = height, maxHeight = height)
}
val firstPlaceable = firstMeasurable.measure(constraints.constrain(firstConstraints))
val secondPlaceable = secondMeasurable.measure(constraints.constrain(secondConstraints))
firstPlaceable.placeRelative(0, 0)
val detailOffsetX =
if (gapOrientation == Orientation.Vertical) {
constraints.maxWidth - secondPlaceable.width
} else {
0
}
val detailOffsetY =
if (gapOrientation == Orientation.Vertical) {
0
} else {
constraints.maxHeight - secondPlaceable.height
}
secondPlaceable.placeRelative(detailOffsetX, detailOffsetY)
}
}
}
/**
* The configuration for which type of folds for a [TwoPane] to automatically avoid.
*/
@JvmInline
public value class FoldAwareConfiguration private constructor(private val value: Int) {
public companion object {
/**
* The [TwoPane] will only be aware of horizontal folds only, splitting the content
* vertically.
*/
public val HorizontalFoldsOnly: FoldAwareConfiguration = FoldAwareConfiguration(0)
/**
* The [TwoPane] will only be aware of vertical folds only, splitting the content
* horizontally.
*/
public val VerticalFoldsOnly: FoldAwareConfiguration = FoldAwareConfiguration(1)
/**
* The [TwoPane] will be aware of both horizontal and vertical folds, splitting the content
* vertically and horizontally respectively.
*/
public val AllFolds: FoldAwareConfiguration = FoldAwareConfiguration(2)
}
}
/**
* Returns the specification for where to place a split in [TwoPane] as a result of
* [TwoPaneStrategy.calculateSplitResult]
*/
public class SplitResult(
/**
* Whether the gap is vertical or horizontal
*/
public val gapOrientation: Orientation,
/**
* The bounds that are nether a `start` pane or an `end` pane, but a separation between those
* two. In case width or height is 0 - it means that the gap itself is a 0 width/height, but the
* place within the layout is still defined.
*
* The [gapBounds] should be defined in local bounds to the [TwoPane].
*/
public val gapBounds: Rect,
)
/**
* A strategy for configuring the [TwoPane] component, that is responsible for the meta-data
* corresponding to the arrangement of the two panes of the layout.
*/
public fun interface TwoPaneStrategy {
/**
* Calculates the split result in local bounds of the [TwoPane].
*
* @param density the [Density] for measuring and laying out
* @param layoutDirection the [LayoutDirection] for measuring and laying out
* @param layoutCoordinates the [LayoutCoordinates] of the [TwoPane]
*/
public fun calculateSplitResult(
density: Density,
layoutDirection: LayoutDirection,
layoutCoordinates: LayoutCoordinates
): SplitResult
}
/**
* A strategy for configuring the [TwoPane] component, that is responsible for the meta-data
* corresponding to the arrangement of the two panes of the layout.
*
* This strategy can be conditional: If `null` is returned from [calculateSplitResult], then this
* strategy did not produce a split result to use, and a different strategy should be used.
*/
private fun interface ConditionalTwoPaneStrategy {
/**
* Calculates the split result in local bounds of the [TwoPane], or `null` if this strategy
* does not apply.
*
* @param density the [Density] for measuring and laying out
* @param layoutDirection the [LayoutDirection] for measuring and laying out
* @param layoutCoordinates the [LayoutCoordinates] of the [TwoPane]
*/
fun calculateSplitResult(
density: Density,
layoutDirection: LayoutDirection,
layoutCoordinates: LayoutCoordinates
): SplitResult?
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at the given [splitFraction] from start, with the given
* [gapWidth].
*/
public fun HorizontalTwoPaneStrategy(
splitFraction: Float,
gapWidth: Dp = 0.dp,
): TwoPaneStrategy = FractionHorizontalTwoPaneStrategy(
splitFraction = splitFraction,
gapWidth = gapWidth
)
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at [splitOffset] either from the start or end based on
* [offsetFromStart], with the given [gapWidth].
*/
public fun HorizontalTwoPaneStrategy(
splitOffset: Dp,
offsetFromStart: Boolean = true,
gapWidth: Dp = 0.dp,
): TwoPaneStrategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = splitOffset,
offsetFromStart = offsetFromStart,
gapWidth = gapWidth
)
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at the given [splitFraction] from top, with the given
* [gapHeight].
*/
public fun VerticalTwoPaneStrategy(
splitFraction: Float,
gapHeight: Dp = 0.dp,
): TwoPaneStrategy = FractionVerticalTwoPaneStrategy(
splitFraction = splitFraction,
gapHeight = gapHeight
)
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at [splitOffset] either from the top or bottom based on
* [offsetFromTop], with the given [gapHeight].
*/
public fun VerticalTwoPaneStrategy(
splitOffset: Dp,
offsetFromTop: Boolean = true,
gapHeight: Dp = 0.dp,
): TwoPaneStrategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = splitOffset,
offsetFromTop = offsetFromTop,
gapHeight = gapHeight
)
/**
* Returns a [TwoPaneStrategy] that will place the slots vertically or horizontally if there is a
* horizontal or vertical fold respectively.
*
* If there is no fold, then the [defaultStrategy] will be used instead.
*/
private fun TwoPaneStrategy(
displayFeatures: List<DisplayFeature>,
defaultStrategy: TwoPaneStrategy,
): TwoPaneStrategy = TwoPaneStrategy(
FoldAwareHorizontalTwoPaneStrategy(displayFeatures),
FoldAwareVerticalTwoPaneStrategy(displayFeatures),
defaultStrategy = defaultStrategy
)
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally if there is a vertical fold.
*
* If there is no fold, then the [defaultStrategy] will be used instead.
*/
private fun HorizontalTwoPaneStrategy(
displayFeatures: List<DisplayFeature>,
defaultStrategy: TwoPaneStrategy,
): TwoPaneStrategy = TwoPaneStrategy(
FoldAwareHorizontalTwoPaneStrategy(displayFeatures),
defaultStrategy = defaultStrategy
)
/**
* Returns a [TwoPaneStrategy] that will place the slots vertically if there is a horizontal fold.
*
* If there is no fold, then the [defaultStrategy] will be used instead.
*/
private fun VerticalTwoPaneStrategy(
displayFeatures: List<DisplayFeature>,
defaultStrategy: TwoPaneStrategy,
): TwoPaneStrategy = TwoPaneStrategy(
FoldAwareVerticalTwoPaneStrategy(displayFeatures),
defaultStrategy = defaultStrategy
)
/**
* Returns a composite [TwoPaneStrategy].
*
* The conditional strategies (if any) will be attempted in order, and their split result used
* if they return one. If none return a split result, then the [defaultStrategy] will be used,
* which guarantees returning a [SplitResult].
*/
private fun TwoPaneStrategy(
vararg conditionalStrategies: ConditionalTwoPaneStrategy,
defaultStrategy: TwoPaneStrategy
): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates ->
conditionalStrategies.firstNotNullOfOrNull { conditionalTwoPaneStrategy ->
conditionalTwoPaneStrategy.calculateSplitResult(
density = density,
layoutDirection = layoutDirection,
layoutCoordinates = layoutCoordinates
)
} ?: defaultStrategy.calculateSplitResult(
density = density,
layoutDirection = layoutDirection,
layoutCoordinates = layoutCoordinates
)
}
/**
* Returns a [ConditionalTwoPaneStrategy] that will place the slots horizontally if there is a
* vertical fold, or `null` if there is no fold.
*/
private fun FoldAwareHorizontalTwoPaneStrategy(
displayFeatures: List<DisplayFeature>,
): ConditionalTwoPaneStrategy = ConditionalTwoPaneStrategy { _, _, layoutCoordinates ->
val verticalFold = displayFeatures.find {
it is FoldingFeature && it.orientation == FoldingFeature.Orientation.VERTICAL
} as FoldingFeature?
if (verticalFold != null &&
(
verticalFold.isSeparating ||
verticalFold.occlusionType == FoldingFeature.OcclusionType.FULL
) &&
verticalFold.bounds.toComposeRect().overlaps(layoutCoordinates.boundsInWindow())
) {
val foldBounds = verticalFold.bounds.toComposeRect()
SplitResult(
gapOrientation = Orientation.Vertical,
gapBounds = Rect(
layoutCoordinates.windowToLocal(foldBounds.topLeft),
layoutCoordinates.windowToLocal(foldBounds.bottomRight)
)
)
} else {
null
}
}
/**
* Returns a [ConditionalTwoPaneStrategy] that will place the slots vertically if there is a
* horizontal fold, or `null` if there is no fold.
*/
private fun FoldAwareVerticalTwoPaneStrategy(
displayFeatures: List<DisplayFeature>,
): ConditionalTwoPaneStrategy = ConditionalTwoPaneStrategy { _, _, layoutCoordinates ->
val horizontalFold = displayFeatures.find {
it is FoldingFeature && it.orientation == FoldingFeature.Orientation.HORIZONTAL
} as FoldingFeature?
if (horizontalFold != null &&
(
horizontalFold.isSeparating ||
horizontalFold.occlusionType == FoldingFeature.OcclusionType.FULL
) &&
horizontalFold.bounds.toComposeRect().overlaps(layoutCoordinates.boundsInWindow())
) {
val foldBounds = horizontalFold.bounds.toComposeRect()
SplitResult(
gapOrientation = Orientation.Horizontal,
gapBounds = Rect(
layoutCoordinates.windowToLocal(foldBounds.topLeft),
layoutCoordinates.windowToLocal(foldBounds.bottomRight)
)
)
} else {
null
}
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at the given [splitFraction] from start, with the given [gapWidth].
*
* This strategy is _not_ fold aware.
*/
internal fun FractionHorizontalTwoPaneStrategy(
splitFraction: Float,
gapWidth: Dp = 0.dp,
): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates ->
val splitX = layoutCoordinates.size.width * when (layoutDirection) {
LayoutDirection.Ltr -> splitFraction
LayoutDirection.Rtl -> 1 - splitFraction
}
val splitWidthPixel = with(density) { gapWidth.toPx() }
SplitResult(
gapOrientation = Orientation.Vertical,
gapBounds = Rect(
left = splitX - splitWidthPixel / 2f,
top = 0f,
right = splitX + splitWidthPixel / 2f,
bottom = layoutCoordinates.size.height.toFloat(),
)
)
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The gap will be placed at [splitOffset] either from the start or end based on
* [offsetFromStart], with the given [gapWidth].
*
* This strategy is _not_ fold aware.
*/
internal fun FixedOffsetHorizontalTwoPaneStrategy(
splitOffset: Dp,
offsetFromStart: Boolean,
gapWidth: Dp = 0.dp,
): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates ->
val splitOffsetPixel = with(density) { splitOffset.toPx() }
val splitX = when (layoutDirection) {
LayoutDirection.Ltr ->
if (offsetFromStart) {
splitOffsetPixel
} else {
layoutCoordinates.size.width - splitOffsetPixel
}
LayoutDirection.Rtl ->
if (offsetFromStart) {
layoutCoordinates.size.width - splitOffsetPixel
} else {
splitOffsetPixel
}
}
val splitWidthPixel = with(density) { gapWidth.toPx() }
SplitResult(
gapOrientation = Orientation.Vertical,
gapBounds = Rect(
left = splitX - splitWidthPixel / 2f,
top = 0f,
right = splitX + splitWidthPixel / 2f,
bottom = layoutCoordinates.size.height.toFloat(),
)
)
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The split will be placed at the given [splitFraction] from start, with the given [gapHeight].
*
* This strategy is _not_ fold aware.
*/
internal fun FractionVerticalTwoPaneStrategy(
splitFraction: Float,
gapHeight: Dp = 0.dp,
): TwoPaneStrategy = TwoPaneStrategy { density, _, layoutCoordinates ->
val splitY = layoutCoordinates.size.height * splitFraction
val splitHeightPixel = with(density) { gapHeight.toPx() }
SplitResult(
gapOrientation = Orientation.Horizontal,
gapBounds = Rect(
left = 0f,
top = splitY - splitHeightPixel / 2f,
right = layoutCoordinates.size.width.toFloat(),
bottom = splitY + splitHeightPixel / 2f,
)
)
}
/**
* Returns a [TwoPaneStrategy] that will place the slots horizontally.
*
* The split will be placed at [splitOffset] either from the top or bottom based on
* [offsetFromTop], with the given [gapHeight].
*
* This strategy is _not_ fold aware.
*/
internal fun FixedOffsetVerticalTwoPaneStrategy(
splitOffset: Dp,
offsetFromTop: Boolean,
gapHeight: Dp = 0.dp,
): TwoPaneStrategy = TwoPaneStrategy { density, _, layoutCoordinates ->
val splitOffsetPixel = with(density) { splitOffset.toPx() }
val splitY =
if (offsetFromTop) {
splitOffsetPixel
} else {
layoutCoordinates.size.height - splitOffsetPixel
}
val splitHeightPixel = with(density) { gapHeight.toPx() }
SplitResult(
gapOrientation = Orientation.Horizontal,
gapBounds = Rect(
left = 0f,
top = splitY - splitHeightPixel / 2f,
right = layoutCoordinates.size.width.toFloat(),
bottom = splitY + splitHeightPixel / 2f,
)
)
}
================================================
FILE: adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/DisplayFeaturesTest.kt
================================================
/*
* 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.google.accompanist.adaptive
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowLayoutInfo
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DisplayFeaturesTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@get:Rule
val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule()
@Test
fun empty_folding_features_is_correct() {
lateinit var displayFeatures: List<DisplayFeature>
composeTestRule.setContent {
displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity)
}
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList()))
composeTestRule.waitForIdle()
assertThat(displayFeatures).isEmpty()
}
@Test
fun single_folding_features_is_correct() {
lateinit var displayFeatures: List<DisplayFeature>
composeTestRule.setContent {
displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity)
}
val fakeFoldingFeature = FoldingFeature(
activity = composeTestRule.activity,
center = 200,
size = 40,
state = FoldingFeature.State.HALF_OPENED,
orientation = FoldingFeature.Orientation.VERTICAL,
)
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(
WindowLayoutInfo(
listOf(
fakeFoldingFeature
)
)
)
composeTestRule.waitForIdle()
assertThat(displayFeatures).hasSize(1)
assertThat(displayFeatures[0]).isEqualTo(fakeFoldingFeature)
}
@Test
fun updating_folding_features_is_correct() {
lateinit var displayFeatures: List<DisplayFeature>
composeTestRule.setContent {
displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity)
}
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList()))
val fakeFoldingFeature = FoldingFeature(
activity = composeTestRule.activity,
center = 200,
size = 40,
state = FoldingFeature.State.HALF_OPENED,
orientation = FoldingFeature.Orientation.VERTICAL,
)
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(
WindowLayoutInfo(
listOf(
fakeFoldingFeature
)
)
)
composeTestRule.waitForIdle()
assertThat(displayFeatures).hasSize(1)
assertThat(displayFeatures[0]).isEqualTo(fakeFoldingFeature)
}
}
================================================
FILE: adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/FoldAwareColumnTest.kt
================================================
/*
* 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.google.accompanist.adaptive
import android.annotation.SuppressLint
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.WindowMetricsCalculator
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import com.google.accompanist.adaptive.FoldAwareColumnScopeInstance.ignoreFold
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class FoldAwareColumnTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@get:Rule
val publisherRule = WindowLayoutInfoPublisherRule()
private val testTag = "FoldAwareColumnTestTag"
private var firstSpacerHeightDp = 0.dp
private var secondSpacerTopPx = 0f
private var secondSpacerBottomPx = 0f
@After
fun cleanUp() {
firstSpacerHeightDp = 0.dp
secondSpacerTopPx = 0f
secondSpacerBottomPx = 0f
}
@Test
fun second_spacer_placed_below_fold_with_hinge() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
val foldBoundsPx = simulateFoldingFeature()
assertEquals(foldBoundsPx.bottom, secondSpacerTopPx)
}
@Test
fun second_spacer_placed_below_fold_with_separating_fold() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
val foldBoundsPx = simulateFoldingFeature(foldSizePx = 0)
assertEquals(foldBoundsPx.bottom, secondSpacerTopPx)
}
@Test
fun second_spacer_placed_below_first_spacer_without_fold() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
composeTestRule.onNodeWithTag(testTag).assertTopPositionInRootIsEqualTo(firstSpacerHeightDp)
}
@Test
fun second_spacer_placed_below_first_spacer_with_non_separating_fold() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
simulateFoldingFeature(foldSizePx = 0, foldState = FoldingFeature.State.FLAT)
composeTestRule.onNodeWithTag(testTag).assertTopPositionInRootIsEqualTo(firstSpacerHeightDp)
}
@Test
fun second_spacer_placed_below_first_spacer_with_vertical_hinge() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
simulateFoldingFeature(foldOrientation = FoldingFeature.Orientation.VERTICAL)
composeTestRule.onNodeWithTag(testTag).assertTopPositionInRootIsEqualTo(firstSpacerHeightDp)
}
@Test
fun second_spacer_placed_below_first_spacer_with_ignore_fold_modifier() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers(secondSpacerModifier = Modifier.ignoreFold())
}
simulateFoldingFeature()
composeTestRule.onNodeWithTag(testTag).assertTopPositionInRootIsEqualTo(firstSpacerHeightDp)
}
@Test
fun even_fold_padding_modifier_applies_around_hinge() {
val foldPaddingDp = 20.dp
lateinit var density: Density
composeTestRule.setContent {
density = LocalDensity.current
FoldAwareColumnWithSpacers(
foldPadding = PaddingValues(vertical = foldPaddingDp)
)
}
val foldBoundsPx = simulateFoldingFeature()
with(density) {
assertEquals(foldBoundsPx.bottom + foldPaddingDp.roundToPx(), secondSpacerTopPx)
}
}
@Test
fun uneven_fold_padding_modifier_applies_around_hinge() {
val foldPaddingBottom = 40.dp
lateinit var density: Density
composeTestRule.setContent {
density = LocalDensity.current
FoldAwareColumnWithSpacers(
foldPadding = PaddingValues(top = 15.dp, bottom = foldPaddingBottom)
)
}
val foldBoundsPx = simulateFoldingFeature()
with(density) {
assertEquals(foldBoundsPx.bottom + foldPaddingBottom.roundToPx(), secondSpacerTopPx)
}
}
@Test
fun layout_bounds_align_with_child_bounds_without_separating_fold() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
val layoutBottomPx = composeTestRule.onRoot()
.fetchSemanticsNode().layoutInfo.coordinates.trueBoundsInWindow().bottom
assertEquals(layoutBottomPx, secondSpacerBottomPx)
}
@Test
fun layout_bounds_contain_child_bounds_when_placed_above_hinge() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers(
firstSpacerHeightPct = 0.1f,
secondSpacerHeightPct = 0.1f
)
}
simulateFoldingFeature()
val layoutBottomPx = composeTestRule.onRoot()
.fetchSemanticsNode().layoutInfo.coordinates.trueBoundsInWindow().bottom
assert(secondSpacerBottomPx <= layoutBottomPx)
}
@Test
fun layout_bounds_contain_child_bounds_when_placed_below_hinge() {
composeTestRule.setContent {
FoldAwareColumnWithSpacers()
}
simulateFoldingFeature()
val layoutBottomPx = composeTestRule.onRoot()
.fetchSemanticsNode().layoutInfo.coordinates.trueBoundsInWindow().bottom
assert(secondSpacerBottomPx <= layoutBottomPx)
}
/**
* Test layout for FoldAwareColumn that includes two spacers with the provided heights
*/
@Composable
@SuppressLint("ModifierParameter")
private fun FoldAwareColumnWithSpacers(
foldPadding: PaddingValues = PaddingValues(),
firstSpacerHeightPct: Float = 0.25f,
secondSpacerHeightPct: Float = 0.25f,
secondSpacerModifier: Modifier = Modifier,
) {
var secondSpacerHeightDp: Dp
val metrics = remember(LocalConfiguration.current) {
WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(composeTestRule.activity)
}
with(LocalDensity.current) {
val windowHeight = metrics.bounds.height().toDp().value
firstSpacerHeightDp = (firstSpacerHeightPct * windowHeight).dp
secondSpacerHeightDp = (secondSpacerHeightPct * windowHeight).dp
}
FoldAwareColumn(
displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity),
foldPadding = foldPadding,
) {
Spacer(
modifier = Modifier.height(firstSpacerHeightDp)
)
Spacer(
modifier = secondSpacerModifier
.height(secondSpacerHeightDp)
.testTag(testTag)
.onGloballyPositioned {
secondSpacerTopPx = it.positionInWindow().y
secondSpacerBottomPx = secondSpacerTopPx + it.size.height
}
)
}
}
/**
* Simulates a Jetpack Window Manager folding feature with the provided properties and returns
* the bounding box of the fold
*/
private fun simulateFoldingFeature(
foldSizePx: Int = 25,
foldState: FoldingFeature.State = FoldingFeature.State.HALF_OPENED,
foldOrientation: FoldingFeature.Orientation = FoldingFeature.Orientation.HORIZONTAL
): Rect {
val fakeFoldingFeature = FoldingFeature(
activity = composeTestRule.activity,
size = foldSizePx,
state = foldState,
orientation = foldOrientation,
)
publisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(listOf(fakeFoldingFeature)))
composeTestRule.waitForIdle()
return fakeFoldingFeature.bounds.toComposeRect()
}
}
================================================
FILE: adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt
================================================
/*
* 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.google.accompanist.adaptive
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.DeviceConfigurationOverride
import androidx.compose.ui.test.ForcedSize
import androidx.compose.ui.test.LayoutDirection
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.then
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toIntRect
import androidx.compose.ui.unit.toOffset
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.core.ExperimentalWindowApi
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import androidx.window.testing.layout.FoldingFeature
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.math.roundToInt
@RunWith(AndroidJUnit4::class)
class TwoPaneTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun fraction_horizontal_renders_correctly_ltr() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Ltr)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionHorizontalTwoPaneStrategy(
splitFraction = 1f / 3f
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(300.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(300.dp, 0.dp),
DpSize(600.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_horizontal_renders_correctly_rtl() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Rtl)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionHorizontalTwoPaneStrategy(
splitFraction = 1f / 3f
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(600.dp, 0.dp),
DpSize(300.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(600.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_horizontal_renders_correctly_with_split_width_ltr() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Ltr)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionHorizontalTwoPaneStrategy(
splitFraction = 1f / 3f,
gapWidth = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(268.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(332.dp, 0.dp),
DpSize(568.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_horizontal_renders_correctly_with_split_width_rtl() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Rtl)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionHorizontalTwoPaneStrategy(
splitFraction = 1f / 3f,
gapWidth = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(632.dp, 0.dp),
DpSize(268.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(568.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_vertical_renders_correctly() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionVerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 400.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 400.dp),
DpSize(900.dp, 800.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fraction_vertical_renders_correctly_with_split_height() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FractionVerticalTwoPaneStrategy(
splitFraction = 1f / 3f,
gapHeight = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 368.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 432.dp),
DpSize(900.dp, 768.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_horizontal_from_start_horizontal_renders_correctly_ltr() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Ltr)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = 200.dp,
offsetFromStart = true,
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(200.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(200.dp, 0.dp),
DpSize(700.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_horizontal_from_start_horizontal_renders_correctly_rtl() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Rtl)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = 200.dp,
offsetFromStart = true,
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(700.dp, 0.dp),
DpSize(200.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(700.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_horizontal_from_start_renders_correctly_with_split_width_ltr() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Ltr)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = 200.dp,
offsetFromStart = true,
gapWidth = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(168.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(232.dp, 0.dp),
DpSize(668.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_horizontal_from_start_renders_correctly_with_split_width_rtl() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp)) then
DeviceConfigurationOverride.LayoutDirection(LayoutDirection.Rtl)
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetHorizontalTwoPaneStrategy(
splitOffset = 200.dp,
offsetFromStart = true,
gapWidth = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(732.dp, 0.dp),
DpSize(168.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(668.dp, 1200.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_vertical_from_top_renders_correctly() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = 300.dp,
offsetFromTop = true
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 300.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 300.dp),
DpSize(900.dp, 900.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_vertical_from_top_renders_correctly_with_split_height() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = 300.dp,
offsetFromTop = true,
gapHeight = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 268.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 332.dp),
DpSize(900.dp, 868.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_vertical_from_bottom_renders_correctly() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = 300.dp,
offsetFromTop = false
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 900.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 900.dp),
DpSize(900.dp, 300.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun fixed_offset_vertical_from_bottom_renders_correctly_with_split_height() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = FixedOffsetVerticalTwoPaneStrategy(
splitOffset = 300.dp,
offsetFromTop = false,
gapHeight = 64.dp
),
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 868.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 932.dp),
DpSize(900.dp, 268.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_fallback_when_no_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = emptyList()
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 400.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 400.dp),
DpSize(900.dp, 800.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_vertical_placing_when_occluding_horizontal_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = listOf(
LocalFoldingFeature(
center = 600.dp,
size = 0.dp,
state = FoldingFeature.State.HALF_OPENED,
orientation = FoldingFeature.Orientation.HORIZONTAL
)
)
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 600.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 600.dp),
DpSize(900.dp, 600.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_vertical_placing_when_separating_horizontal_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatures = DelegateList {
fakeDisplayFeatures(
density = density,
twoPaneCoordinates = twoPaneCoordinates,
localFoldingFeatures = listOf(
LocalFoldingFeature(
center = 600.dp,
size = 60.dp,
state = FoldingFeature.State.FLAT,
orientation = FoldingFeature.Orientation.HORIZONTAL
)
)
)
}
composeTestRule.setContent {
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(900.dp, 1200.dp))
) {
density = LocalDensity.current
TwoPane(
first = {
Spacer(
Modifier
.background(Color.Red)
.fillMaxSize()
.onPlaced { firstCoordinates = it }
)
},
second = {
Spacer(
Modifier
.background(Color.Blue)
.fillMaxSize()
.onPlaced { secondCoordinates = it }
)
},
strategy = VerticalTwoPaneStrategy(
splitFraction = 1f / 3f
),
displayFeatures = displayFeatures,
modifier = Modifier.onPlaced { twoPaneCoordinates = it }
)
}
}
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 0.dp),
DpSize(900.dp, 570.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(firstCoordinates),
1f
)
compareRectWithTolerance(
with(density) {
DpRect(
DpOffset(0.dp, 630.dp),
DpSize(900.dp, 570.dp)
).toRect().round().toRect()
},
twoPaneCoordinates.localBoundingBoxOf(secondCoordinates),
1f
)
}
@Test
fun two_pane_strategy_uses_fallback_when_non_occluding_horizontal_fold_present() {
lateinit var density: Density
lateinit var twoPaneCoordinates: LayoutCoordinates
lateinit var firstCoordinates: LayoutCoordinates
lateinit var secondCoordinates: LayoutCoordinates
val displayFeatu
gitextract_cgrc05nu/
├── .allstar/
│ └── binary_artifacts.yaml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── adaptive-bug-report.md
│ │ ├── bug_report.md
│ │ ├── config.yml
│ │ ├── general-bug-report.md
│ │ ├── general-other-bug-report.md
│ │ ├── navigation-material-bug-report.md
│ │ ├── permissions-bug-report.md
│ │ └── testharness-bug-report.md
│ ├── auto-merge.yml
│ ├── ci-gradle.properties
│ ├── pull_request_template.md
│ ├── release-drafter.yml
│ └── workflows/
│ ├── automerger.yml
│ ├── build-snapshot.yml
│ ├── build.yml
│ ├── issues-stale.yml
│ ├── publish-docs.yml
│ └── update-release.yml
├── .gitignore
├── .idea/
│ ├── codeStyles/
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── copyright/
│ │ ├── AOSP.xml
│ │ └── profiles_settings.xml
│ ├── inspectionProfiles/
│ │ ├── ktlint.xml
│ │ └── profiles_settings.xml
│ ├── kotlinScripting.xml
│ ├── kotlinc.xml
│ ├── runConfigurations.xml
│ └── vcs.xml
├── ASSETS_LICENSE.txt
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── adaptive/
│ ├── README.md
│ ├── api/
│ │ └── current.api
│ ├── build.gradle.kts
│ ├── gradle.properties
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── adaptive/
│ │ ├── DisplayFeatures.kt
│ │ ├── FoldAwareColumn.kt
│ │ ├── FoldAwareColumnScope.kt
│ │ ├── RowColumnImpl.kt
│ │ ├── RowColumnMeasurementHelper.kt
│ │ └── TwoPane.kt
│ ├── sharedTest/
│ │ └── kotlin/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── adaptive/
│ │ ├── DisplayFeaturesTest.kt
│ │ ├── FoldAwareColumnTest.kt
│ │ └── TwoPaneTest.kt
│ └── test/
│ └── resources/
│ └── robolectric.properties
├── build-logic/
│ ├── convention/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── kotlin/
│ │ ├── AndroidLibraryComposeConventionPlugin.kt
│ │ ├── AndroidLibraryConventionPlugin.kt
│ │ ├── AndroidLibraryPublishedConventionPlugin.kt
│ │ ├── AndroidLintConventionPlugin.kt
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ ├── AndroidCompose.kt
│ │ ├── BundleInsideHelper.kt
│ │ ├── KotlinAndroid.kt
│ │ └── ProjectExtensions.kt
│ ├── gradle.properties
│ └── settings.gradle.kts
├── build.gradle
├── checksum.sh
├── docs/
│ ├── adaptive.md
│ ├── appcompat-theme.md
│ ├── drawablepainter.md
│ ├── migration.md
│ ├── navigation-animation.md
│ ├── navigation-material.md
│ ├── permissions.md
│ ├── systemuicontroller.md
│ ├── updating.md
│ ├── using-snapshot-version.md
│ └── web.md
├── drawablepainter/
│ ├── README.md
│ ├── api/
│ │ └── current.api
│ ├── build.gradle.kts
│ ├── gradle.properties
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── google/
│ └── accompanist/
│ └── drawablepainter/
│ └── DrawablePainter.kt
├── generate_docs.sh
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── images/
│ └── Social.sketch
├── internal-testutils/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── internal/
│ │ └── test/
│ │ ├── ActivityScenario.kt
│ │ ├── Assertions.kt
│ │ ├── IgnoreOnRobolectric.kt
│ │ ├── TestUtils.kt
│ │ └── WaitUntil.kt
│ └── res/
│ └── values/
│ └── themes.xml
├── mkdocs.yml
├── permissions/
│ ├── README.md
│ ├── api/
│ │ └── current.api
│ ├── build.gradle.kts
│ ├── gradle.properties
│ └── src/
│ ├── androidTest/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── permissions/
│ │ ├── FakeTests.kt
│ │ ├── MultipleAndSinglePermissionsTest.kt
│ │ ├── MultiplePermissionsStateTest.kt
│ │ ├── PermissionStateTest.kt
│ │ ├── RequestMultiplePermissionsTest.kt
│ │ ├── RequestPermissionTest.kt
│ │ ├── TestUtils.kt
│ │ └── test/
│ │ ├── EmptyPermissionsTestActivity.kt
│ │ └── PermissionsTestActivity.kt
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── google/
│ └── accompanist/
│ └── permissions/
│ ├── MultiplePermissionsState.kt
│ ├── MutableMultiplePermissionsState.kt
│ ├── MutablePermissionState.kt
│ ├── PermissionState.kt
│ └── PermissionsUtil.kt
├── permissions-lint/
│ ├── README.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── google/
│ │ │ └── accompanist/
│ │ │ └── permissions/
│ │ │ └── lint/
│ │ │ ├── PermissionsIssueRegistry.kt
│ │ │ ├── PermissionsLaunchDetector.kt
│ │ │ └── util/
│ │ │ ├── ComposableUtils.kt
│ │ │ ├── KotlinMetadataUtils.kt
│ │ │ ├── Names.kt
│ │ │ └── PsiUtils.kt
│ │ └── resources/
│ │ └── META-INF/
│ │ └── services/
│ │ └── com.android.tools.lint.client.api.IssueRegistry
│ └── test/
│ └── java/
│ └── com/
│ └── google/
│ └── accompanist/
│ └── permissions/
│ └── lint/
│ └── PermissionsLaunchDetectorTest.kt
├── release/
│ ├── secring.gpg.aes
│ ├── signing-cleanup.sh
│ ├── signing-setup.sh
│ └── signing.properties.aes
├── sample/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── google/
│ │ └── accompanist/
│ │ └── sample/
│ │ ├── ImageLoadingSampleUtils.kt
│ │ ├── MainActivity.kt
│ │ ├── MainScreen.kt
│ │ ├── Theme.kt
│ │ ├── adaptive/
│ │ │ ├── BasicTwoPaneSample.kt
│ │ │ ├── DraggableFoldAwareColumnSample.kt
│ │ │ ├── HorizontalTwoPaneSample.kt
│ │ │ ├── NavDrawerFoldAwareColumnSample.kt
│ │ │ ├── NavRailFoldAwareColumnSample.kt
│ │ │ └── VerticalTwoPaneSample.kt
│ │ ├── drawablepainter/
│ │ │ └── DocSamples.kt
│ │ └── permissions/
│ │ ├── RequestLocationPermissionsSample.kt
│ │ ├── RequestMultiplePermissionsSample.kt
│ │ └── RequestPermissionSample.kt
│ └── res/
│ ├── drawable/
│ │ ├── ic_launcher_background.xml
│ │ └── rectangle.xml
│ ├── drawable-v24/
│ │ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── values/
│ │ └── strings.xml
│ └── values-ar/
│ └── strings.xml
├── scripts/
│ └── run-tests.sh
├── settings.gradle.kts
└── spotless/
├── copyright.txt
└── greclipse.properties
Condensed preview — 157 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (557K chars).
[
{
"path": ".allstar/binary_artifacts.yaml",
"chars": 171,
"preview": "# Exemption reason: This repo uses binary artifacts to ship gradle.jar for users. It does not allow any others.\n# Exempt"
},
{
"path": ".github/ISSUE_TEMPLATE/adaptive-bug-report.md",
"chars": 220,
"preview": "---\nname: Adaptive bug report\nabout: Create a report about adaptive\ntitle: \"[Adaptive]\"\nlabels: adaptive\nassignees: alex"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 669,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n## Describe the "
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 28,
"preview": "blank_issues_enabled: false\n"
},
{
"path": ".github/ISSUE_TEMPLATE/general-bug-report.md",
"chars": 741,
"preview": "---\nname: General Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n## Descr"
},
{
"path": ".github/ISSUE_TEMPLATE/general-other-bug-report.md",
"chars": 747,
"preview": "---\nname: General/Other bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n##"
},
{
"path": ".github/ISSUE_TEMPLATE/navigation-material-bug-report.md",
"chars": 241,
"preview": "---\nname: Navigation Material bug report\nabout: Create a report to help us improve\ntitle: \"[Navigation Material] \"\nlabel"
},
{
"path": ".github/ISSUE_TEMPLATE/permissions-bug-report.md",
"chars": 228,
"preview": "---\nname: Permissions bug report\nabout: Create a report to help us improve\ntitle: \"[Permissions] \"\nlabels: ''\nassignees:"
},
{
"path": ".github/ISSUE_TEMPLATE/testharness-bug-report.md",
"chars": 235,
"preview": "---\nname: Test harness bug report\nabout: Create a report about test harness\ntitle: \"[Test Harness]\"\nlabels: testharness\n"
},
{
"path": ".github/auto-merge.yml",
"chars": 157,
"preview": "# Config for github.com/bobvanderlinden/probot-auto-merge\nminApprovals:\n COLLABORATOR: 1\nrequiredLabels:\n - automerge\n"
},
{
"path": ".github/ci-gradle.properties",
"chars": 865,
"preview": "#\n# Copyright 2019 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this"
},
{
"path": ".github/pull_request_template.md",
"chars": 84,
"preview": "### Please add the library name to the PR title. Example: \"[Insets] Fixes typo\" ###\n"
},
{
"path": ".github/release-drafter.yml",
"chars": 120,
"preview": "name-template: 'v$NEXT_PATCH_VERSION 🌈'\ntag-template: 'v$NEXT_PATCH_VERSION'\ntemplate: |\n ## What’s Changed\n\n $CHANGES"
},
{
"path": ".github/workflows/automerger.yml",
"chars": 522,
"preview": "name: main to snapshot auto merger\n\non:\n push:\n branches:\n - main\n\njobs:\n automerge:\n runs-on: ubuntu-lates"
},
{
"path": ".github/workflows/build-snapshot.yml",
"chars": 5807,
"preview": "name: Build & test (snapshot)\n\non:\n push:\n branches:\n - snapshot\n paths-ignore:\n - '**.md'\n pull_reque"
},
{
"path": ".github/workflows/build.yml",
"chars": 5872,
"preview": "name: Build & test\n\non:\n push:\n branches:\n - main\n - compose-1.0\n - compose-1.1\n - compose-1.2\n "
},
{
"path": ".github/workflows/issues-stale.yml",
"chars": 519,
"preview": "name: 'Close stale issues and PRs'\non:\n schedule:\n - cron: '15 3 * * *'\n\njobs:\n stale:\n runs-on: ubuntu-latest\n "
},
{
"path": ".github/workflows/publish-docs.yml",
"chars": 1078,
"preview": "name: Publish docs\n\non:\n push:\n tags:\n - v*\n workflow_dispatch:\n\njobs:\n deploy_docs:\n runs-on: ubuntu-late"
},
{
"path": ".github/workflows/update-release.yml",
"chars": 362,
"preview": "name: Update release\n\non:\n push:\n branches:\n - main\n\njobs:\n update_draft_release:\n runs-on: ubuntu-latest\n "
},
{
"path": ".gitignore",
"chars": 700,
"preview": "# Gradle\n.gradle\nbuild/\n\ncaptures\n\n/local.properties\n\n# IntelliJ .idea folder\n.idea/workspace.xml\n.idea/libraries\n.idea/"
},
{
"path": ".idea/codeStyles/Project.xml",
"chars": 4461,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <code_scheme name=\"Project\" version=\"173\">\n <JetCodeStyleSettings>"
},
{
"path": ".idea/codeStyles/codeStyleConfig.xml",
"chars": 143,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <state>\n <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n "
},
{
"path": ".idea/copyright/AOSP.xml",
"chars": 813,
"preview": "<component name=\"CopyrightManager\">\n <copyright>\n <option name=\"notice\" value=\"Copyright &#36;today.year The And"
},
{
"path": ".idea/copyright/profiles_settings.xml",
"chars": 78,
"preview": "<component name=\"CopyrightManager\">\n <settings default=\"AOSP\" />\n</component>"
},
{
"path": ".idea/inspectionProfiles/ktlint.xml",
"chars": 359,
"preview": "<component name=\"InspectionProjectProfileManager\">\n <profile version=\"1.0\">\n <option name=\"myName\" value=\"ktlint\" />"
},
{
"path": ".idea/inspectionProfiles/profiles_settings.xml",
"chars": 172,
"preview": "<component name=\"InspectionProjectProfileManager\">\n <settings>\n <option name=\"PROJECT_PROFILE\" value=\"ktlint\" />\n "
},
{
"path": ".idea/kotlinScripting.xml",
"chars": 186,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"KotlinScriptingSettings\">\n <option na"
},
{
"path": ".idea/kotlinc.xml",
"chars": 284,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"Kotlin2JvmCompilerArguments\">\n <optio"
},
{
"path": ".idea/runConfigurations.xml",
"chars": 964,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"RunConfigurationProducerService\">\n <o"
},
{
"path": ".idea/vcs.xml",
"chars": 167,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"VcsDirectoryMappings\">\n <mapping dire"
},
{
"path": "ASSETS_LICENSE.txt",
"chars": 4133,
"preview": "All font files are licensed under the SIL OPEN FONT LICENSE license. All other files are licensed under the Apache 2 lic"
},
{
"path": "CONTRIBUTING.md",
"chars": 1660,
"preview": "# How to Contribute\n\nWe'd love to accept your patches and contributions to this project. There are\njust a few small guid"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 6106,
"preview": "\n\nAccompanist is a group of libraries that aim to supplement [Jetpack Compose][compo"
},
{
"path": "adaptive/README.md",
"chars": 700,
"preview": "# Adaptive utilities for Jetpack Compose\n\n[;\n#"
},
{
"path": "gradlew",
"chars": 8473,
"preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
},
{
"path": "gradlew.bat",
"chars": 2868,
"preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
},
{
"path": "internal-testutils/build.gradle.kts",
"chars": 1111,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "internal-testutils/src/main/AndroidManifest.xml",
"chars": 790,
"preview": "<!--\n ~ Copyright 2021 The Android Open Source Project\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"Lic"
},
{
"path": "internal-testutils/src/main/java/com/google/accompanist/internal/test/ActivityScenario.kt",
"chars": 923,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "internal-testutils/src/main/java/com/google/accompanist/internal/test/Assertions.kt",
"chars": 2748,
"preview": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "internal-testutils/src/main/java/com/google/accompanist/internal/test/IgnoreOnRobolectric.kt",
"chars": 803,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "internal-testutils/src/main/java/com/google/accompanist/internal/test/TestUtils.kt",
"chars": 1236,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "internal-testutils/src/main/java/com/google/accompanist/internal/test/WaitUntil.kt",
"chars": 1321,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "internal-testutils/src/main/res/values/themes.xml",
"chars": 943,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2021 The Android Open Source Project\n ~\n ~ Licensed under th"
},
{
"path": "mkdocs.yml",
"chars": 1580,
"preview": "# Project information\nsite_name: 'Accompanist'\nsite_description: 'A group of libraries to help write Jetpack Compose app"
},
{
"path": "permissions/README.md",
"chars": 705,
"preview": "# Permissions for Jetpack Compose\n\n[;\n#"
},
{
"path": "permissions-lint/src/test/java/com/google/accompanist/permissions/lint/PermissionsLaunchDetectorTest.kt",
"chars": 11203,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "release/signing-cleanup.sh",
"chars": 656,
"preview": "#!/bin/sh\n\n# Copyright 2021 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lic"
},
{
"path": "release/signing-setup.sh",
"chars": 983,
"preview": "#!/bin/bash\n\n# Copyright 2021 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"L"
},
{
"path": "sample/build.gradle.kts",
"chars": 2265,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/AndroidManifest.xml",
"chars": 6044,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2022 The Android Open Source Project\n ~\n ~ Licensed under th"
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/ImageLoadingSampleUtils.kt",
"chars": 1237,
"preview": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/MainActivity.kt",
"chars": 4219,
"preview": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/MainScreen.kt",
"chars": 3476,
"preview": "/*\n * Copyright 2024 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/Theme.kt",
"chars": 2004,
"preview": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt",
"chars": 3568,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/adaptive/DraggableFoldAwareColumnSample.kt",
"chars": 4349,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/adaptive/HorizontalTwoPaneSample.kt",
"chars": 3038,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/adaptive/NavDrawerFoldAwareColumnSample.kt",
"chars": 4044,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/adaptive/NavRailFoldAwareColumnSample.kt",
"chars": 3517,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/adaptive/VerticalTwoPaneSample.kt",
"chars": 3031,
"preview": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/drawablepainter/DocSamples.kt",
"chars": 1216,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/permissions/RequestLocationPermissionsSample.kt",
"chars": 3558,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/permissions/RequestMultiplePermissionsSample.kt",
"chars": 4041,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/java/com/google/accompanist/sample/permissions/RequestPermissionSample.kt",
"chars": 2470,
"preview": "/*\n * Copyright 2021 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "sample/src/main/res/drawable/ic_launcher_background.xml",
"chars": 6241,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2020 The Android Open Source Project\n ~\n ~ Licensed under th"
},
{
"path": "sample/src/main/res/drawable/rectangle.xml",
"chars": 787,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2020 The Android Open Source Project\n ~\n ~ Licensed under th"
},
{
"path": "sample/src/main/res/drawable-v24/ic_launcher_foreground.xml",
"chars": 2515,
"preview": "<!--\n ~ Copyright 2020 The Android Open Source Project\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"Lic"
},
{
"path": "sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 907,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2020 The Android Open Source Project\n ~\n ~ Licensed under th"
},
{
"path": "sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 907,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright 2020 The Android Open Source Project\n ~\n ~ Licensed under th"
},
{
"path": "sample/src/main/res/values/strings.xml",
"chars": 4808,
"preview": "<!--\n ~ Copyright 2022 The Android Open Source Project\n ~\n ~ Licensed under the Apache License, Version 2.0 (the \"Lic"
},
{
"path": "sample/src/main/res/values-ar/strings.xml",
"chars": 757,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n ~ Copyright 2022 The Android Open Source Project\n ~\n ~ Licensed under the"
},
{
"path": "scripts/run-tests.sh",
"chars": 2700,
"preview": "#!/bin/bash\n\n# Copyright 2021 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"L"
},
{
"path": "settings.gradle.kts",
"chars": 1363,
"preview": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
},
{
"path": "spotless/copyright.txt",
"chars": 619,
"preview": "/*\n * Copyright $YEAR The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
},
{
"path": "spotless/greclipse.properties",
"chars": 1852,
"preview": "#\n# Copyright 2020 The Android Open Source Project\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n#"
}
]
// ... and 4 more files (download for full content)
About this extraction
This page contains the full source code of the google/accompanist GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 157 files (515.2 KB), approximately 114.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.