├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── support_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── autoapproval.yml ├── blunderbuss.yml ├── dependabot.yml ├── stale.yml ├── sync-repo-settings.yaml └── workflows │ ├── automerge-docs.yml │ ├── docs.yml │ ├── instrumentation-test.yml │ ├── lint-report.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .releaserc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── build-logic ├── .gitignore ├── convention │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── PublishingConventionPlugin.kt └── settings.gradle.kts ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.defaults.properties ├── maps-app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── google │ │ └── maps │ │ └── android │ │ └── compose │ │ ├── GoogleMapViewTests.kt │ │ ├── MapInColumnTests.kt │ │ ├── MapsInLazyColumnTest.kt │ │ ├── StreetViewTests.kt │ │ └── TestUtils.kt │ ├── debug │ └── screenshotTest │ │ └── reference │ │ └── com │ │ └── google │ │ └── maps │ │ └── android │ │ └── compose │ │ └── ScaleBarTest │ │ ├── PreviewDisappearingScaleBar_0.png │ │ └── PreviewScaleBar_0.png │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── google │ │ │ └── maps │ │ │ └── android │ │ │ └── compose │ │ │ ├── AccessibilityActivity.kt │ │ │ ├── BasicMapActivity.kt │ │ │ ├── CustomControlsActivity.kt │ │ │ ├── LocationTrackingActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MapInColumnActivity.kt │ │ │ ├── MapsInLazyColumnActivity.kt │ │ │ ├── RecompositionActivity.kt │ │ │ ├── ScaleBarActivity.kt │ │ │ ├── StreetViewActivity.kt │ │ │ ├── markerexamples │ │ │ ├── AdvancedMarkersActivity.kt │ │ │ ├── MarkerClusteringActivity.kt │ │ │ ├── draggablemarkerscollectionwithpolygon │ │ │ │ └── DraggableMarkersCollectionWithPolygonActivity.kt │ │ │ ├── markerdragevents │ │ │ │ └── MarkerDragEventsActivity.kt │ │ │ ├── markerscollection │ │ │ │ └── MarkersCollectionActivity.kt │ │ │ ├── syncingdraggablemarkerwithdatamodel │ │ │ │ └── SyncingDraggableMarkerWithDataModelActivity.kt │ │ │ └── updatingnodragmarkerwithdatamodel │ │ │ │ └── UpdatingNoDragMarkerWithDataModelActivity.kt │ │ │ └── theme │ │ │ └── Theme.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ └── values │ │ ├── strings.xml │ │ └── themes.xml │ └── screenshotTest │ └── java │ └── com │ └── google │ └── maps │ └── android │ └── compose │ └── ScaleBarTest.kt ├── maps-compose-utils ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── google │ └── maps │ └── android │ └── compose │ └── clustering │ ├── ClusterRenderer.kt │ └── Clustering.kt ├── maps-compose-widgets ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── google │ └── maps │ └── android │ └── compose │ └── widgets │ └── ScaleBar.kt ├── maps-compose ├── .gitignore ├── build.gradle.kts ├── compose_compiler_stability_config.conf ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── google │ │ └── maps │ │ └── android │ │ └── compose │ │ ├── CameraMoveStartedReason.kt │ │ ├── CameraPositionState.kt │ │ ├── Circle.kt │ │ ├── ComposeInfoWindowAdapter.kt │ │ ├── GoogleMap.kt │ │ ├── GoogleMapComposable.kt │ │ ├── GroundOverlay.kt │ │ ├── InputHandler.kt │ │ ├── MapApplier.kt │ │ ├── MapClickListeners.kt │ │ ├── MapComposeViewRender.kt │ │ ├── MapEffect.kt │ │ ├── MapProperties.kt │ │ ├── MapType.kt │ │ ├── MapUiSettings.kt │ │ ├── MapUpdater.kt │ │ ├── MapsComposeExperimentalApi.kt │ │ ├── Marker.kt │ │ ├── Polygon.kt │ │ ├── Polyline.kt │ │ ├── ReattachClickListeners.kt │ │ ├── RememberComposeBitmapDescriptor.kt │ │ ├── TileOverlay.kt │ │ └── streetview │ │ ├── StreetView.kt │ │ ├── StreetViewCameraPositionState.kt │ │ ├── StreetViewPanoramaApplier.kt │ │ ├── StreetViewPanoramaEventListeners.kt │ │ └── StreetViewPanoramaUpdater.kt │ └── res │ └── values │ └── ids.xml └── settings.gradle.kts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 16 | 17 | .github/ @googlemaps/googlemaps-admin 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'type: bug, triage me' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Thanks for stopping by to let us know something could be better! 11 | 12 | --- 13 | **PLEASE READ** 14 | 15 | If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/). This will ensure a timely response. 16 | 17 | Discover additional support services for the Google Maps Platform, including developer communities, technical guidance, and expert support at the Google Maps Platform [support resources page](https://developers.google.com/maps/support/). 18 | 19 | If your bug or feature request is not related to this particular library, please visit the Google Maps Platform [issue trackers](https://developers.google.com/maps/support/#issue_tracker). 20 | 21 | Check for answers on StackOverflow with the [google-maps](http://stackoverflow.com/questions/tagged/google-maps) tag. 22 | 23 | --- 24 | 25 | Please be sure to include as much information as possible: 26 | 27 | #### Environment details 28 | 29 | 1. Specify the API at the beginning of the title (for example, "Places: ...") 30 | 2. OS type and version 31 | 3. Library version and other environment information 32 | 33 | #### Steps to reproduce 34 | 35 | 1. ? 36 | 37 | #### Code example 38 | 39 | ``` 40 | # example 41 | ``` 42 | 43 | #### Stack trace 44 | ``` 45 | # example 46 | ``` 47 | 48 | Following these steps will guarantee the quickest resolution possible. 49 | 50 | Thanks! 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this library 4 | title: '' 5 | labels: 'type: feature request, triage me' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Thanks for stopping by to let us know something could be better! 11 | 12 | --- 13 | **PLEASE READ** 14 | 15 | If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/). This will ensure a timely response. 16 | 17 | Discover additional support services for the Google Maps Platform, including developer communities, technical guidance, and expert support at the Google Maps Platform [support resources page](https://developers.google.com/maps/support/). 18 | 19 | If your bug or feature request is not related to this particular library, please visit the Google Maps Platform [issue trackers](https://developers.google.com/maps/support/#issue_tracker). 20 | 21 | Check for answers on StackOverflow with the [google-maps](http://stackoverflow.com/questions/tagged/google-maps) tag. 22 | 23 | --- 24 | 25 | **Is your feature request related to a problem? Please describe.** 26 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 27 | 28 | **Describe the solution you'd like** 29 | A clear and concise description of what you want to happen. 30 | 31 | **Describe alternatives you've considered** 32 | A clear and concise description of any alternative solutions or features you've considered. 33 | 34 | **Additional context** 35 | Add any other context or screenshots about the feature request here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support request 3 | about: If you have a support contract with Google, please create an issue in the Google 4 | Cloud Support console. 5 | title: '' 6 | labels: 'triage me, type: question' 7 | assignees: '' 8 | 9 | --- 10 | 11 | **PLEASE READ** 12 | 13 | If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/). This will ensure a timely response. 14 | 15 | Discover additional support services for the Google Maps Platform, including developer communities, technical guidance, and expert support at the Google Maps Platform [support resources page](https://developers.google.com/maps/support/). 16 | 17 | If your bug or feature request is not related to this particular library, please visit the Google Maps Platform [issue trackers](https://developers.google.com/maps/support/#issue_tracker). 18 | 19 | Check for answers on StackOverflow with the [google-maps](http://stackoverflow.com/questions/tagged/google-maps) tag. 20 | 21 | --- 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request 3 | about: Create a pull request 4 | label: 'triage me' 5 | --- 6 | Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: 7 | - [ ] Make sure to open a GitHub issue as a bug/feature request before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea 8 | - [ ] Ensure the tests and linter pass 9 | - [ ] Code coverage does not decrease (if any source code was changed) 10 | - [ ] Appropriate docs were updated (if necessary) 11 | 12 | Fixes # 🦕 13 | -------------------------------------------------------------------------------- /.github/autoapproval.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from_owner: 17 | - dependabot 18 | - dependabot[bot] 19 | - dependabot-preview[bot] 20 | - googlemaps-bot 21 | 22 | required_labels: 23 | - dependencies 24 | - docs 25 | 26 | required_labels_mode: one_of 27 | 28 | apply_labels: 29 | - merge 30 | -------------------------------------------------------------------------------- /.github/blunderbuss.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # assign_issues: 16 | # - 17 | assign_prs: 18 | - dkhawk 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | version: 2 16 | updates: 17 | - package-ecosystem: "gradle" # See documentation for possible values 18 | directory: "/" # Location of package manifests 19 | schedule: 20 | interval: "weekly" 21 | open-pull-requests-limit: 10 22 | commit-message: 23 | prefix: chore(deps) 24 | - package-ecosystem: "github-actions" 25 | directory: "/" # Location of package manifests 26 | schedule: 27 | interval: "weekly" 28 | open-pull-requests-limit: 10 29 | commit-message: 30 | prefix: chore(deps) 31 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Configuration for probot-stale - https://github.com/probot/stale 16 | 17 | # Number of days of inactivity before an Issue or Pull Request becomes stale 18 | daysUntilStale: 120 19 | 20 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 21 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 22 | daysUntilClose: 180 23 | 24 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 25 | onlyLabels: [] 26 | 27 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 28 | exemptLabels: 29 | - pinned 30 | - "type: bug" 31 | 32 | # Set to true to ignore issues in a project (defaults to false) 33 | exemptProjects: false 34 | 35 | # Set to true to ignore issues in a milestone (defaults to false) 36 | exemptMilestones: false 37 | 38 | # Set to true to ignore issues with an assignee (defaults to false) 39 | exemptAssignees: false 40 | 41 | # Label to use when marking as stale 42 | staleLabel: "stale" 43 | 44 | # Comment to post when marking as stale. Set to `false` to disable 45 | markComment: > 46 | This issue has been automatically marked as stale because it has not had 47 | recent activity. Please comment here if it is still valid so that we can 48 | reprioritize. Thank you! 49 | 50 | # Comment to post when removing the stale label. 51 | # unmarkComment: > 52 | # Your comment here. 53 | 54 | # Comment to post when closing a stale Issue or Pull Request. 55 | closeComment: > 56 | Closing this. Please reopen if you believe it should be addressed. Thank you for your contribution. 57 | 58 | # Limit the number of actions per hour, from 1-30. Default is 30 59 | limitPerRun: 10 60 | 61 | # Limit to only `issues` or `pulls` 62 | only: issues 63 | 64 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 65 | # pulls: 66 | # daysUntilStale: 30 67 | # markComment: > 68 | # This pull request has been automatically marked as stale because it has not had 69 | # recent activity. It will be closed if no further activity occurs. Thank you 70 | # for your contributions. 71 | 72 | # issues: 73 | # exemptLabels: 74 | # - confirmed 75 | -------------------------------------------------------------------------------- /.github/sync-repo-settings.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings 16 | 17 | rebaseMergeAllowed: true 18 | squashMergeAllowed: true 19 | mergeCommitAllowed: false 20 | deleteBranchOnMerge: true 21 | branchProtectionRules: 22 | - pattern: main 23 | isAdminEnforced: false 24 | requiresStrictStatusChecks: false 25 | requiredStatusCheckContexts: 26 | - 'cla/google' 27 | - 'test' 28 | - 'snippet-bot check' 29 | - 'header-check' 30 | requiredApprovingReviewCount: 1 31 | requiresCodeOwnerReviews: true 32 | - pattern: master 33 | isAdminEnforced: false 34 | requiresStrictStatusChecks: false 35 | requiredStatusCheckContexts: 36 | - 'cla/google' 37 | - 'test' 38 | - 'snippet-bot check' 39 | - 'header-check' 40 | requiredApprovingReviewCount: 1 41 | requiresCodeOwnerReviews: true 42 | permissionRules: 43 | - team: admin 44 | permission: admin 45 | -------------------------------------------------------------------------------- /.github/workflows/automerge-docs.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Dependabot 16 | on: pull_request 17 | 18 | permissions: 19 | contents: write 20 | 21 | jobs: 22 | test: 23 | uses: ./.github/workflows/test.yml 24 | dependabot: 25 | needs: test 26 | runs-on: ubuntu-latest 27 | if: ${{ github.actor == 'googlemaps-bot' }} 28 | env: 29 | PR_URL: ${{github.event.pull_request.html_url}} 30 | GITHUB_TOKEN: ${{secrets.SYNCED_GITHUB_TOKEN_REPO}} 31 | steps: 32 | - name: approve 33 | run: gh pr review --approve "$PR_URL" 34 | - name: merge 35 | run: gh pr merge --auto --squash --delete-branch "$PR_URL" 36 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A workflow that updates the gh-pages branch whenever a new release is made 16 | name: Update documentation 17 | 18 | on: 19 | push: 20 | branches: [ main ] 21 | repository_dispatch: 22 | types: [gh-pages] 23 | workflow_dispatch: 24 | 25 | jobs: 26 | gh-page-sync: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 31 | - name: Checkout Repo 32 | uses: actions/checkout@v4 33 | 34 | - name: Gradle Wrapper Validation 35 | uses: gradle/actions/wrapper-validation@v4 36 | 37 | - name: Set up JDK 17 38 | uses: actions/setup-java@v4.6.0 39 | with: 40 | java-version: '21' 41 | distribution: 'adopt' 42 | 43 | # Run dokka and create tar 44 | - name: Generate documentation 45 | run: | 46 | ./gradlew dokkaHtmlMultiModule 47 | 48 | echo "Creating tar for generated docs" 49 | cd $GITHUB_WORKSPACE/build/dokka/htmlMultiModule && tar cvf ~/maps-compose-docs.tar . 50 | 51 | echo "Unpacking tar into gh-pages branch" 52 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* 53 | cd $GITHUB_WORKSPACE && git checkout gh-pages && tar xvf ~/maps-compose-docs.tar 54 | 55 | # Commit changes and create a PR 56 | - name: PR Changes 57 | uses: peter-evans/create-pull-request@v7 58 | with: 59 | token: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} 60 | commit-message: 'docs: Update docs' 61 | author: googlemaps-bot 62 | committer: googlemaps-bot 63 | labels: | 64 | docs 65 | automerge 66 | title: 'docs: Update docs' 67 | body: | 68 | Updated GitHub pages with latest from `./gradlew dokkaHtml`. 69 | branch: googlemaps-bot/update_gh_pages 70 | -------------------------------------------------------------------------------- /.github/workflows/instrumentation-test.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A workflow that runs tests on every new pull request 16 | name: Run instrumentation tests 17 | 18 | on: 19 | repository_dispatch: 20 | types: [test] 21 | push: 22 | branches-ignore: ['gh-pages'] 23 | pull_request: 24 | branches-ignore: ['gh-pages'] 25 | workflow_dispatch: 26 | 27 | jobs: 28 | run-instrumentation-test: 29 | runs-on: macOS-latest-large # enables hardware acceleration in the virtual machine 30 | permissions: 31 | pull-requests: write 32 | timeout-minutes: 30 33 | steps: 34 | - name: Checkout Repo 35 | uses: actions/checkout@v4 36 | 37 | - name: Gradle Wrapper Validation 38 | uses: gradle/actions/wrapper-validation@v4 39 | 40 | - name: Set up JDK 17 41 | uses: actions/setup-java@v4.6.0 42 | with: 43 | java-version: '21' 44 | distribution: 'adopt' 45 | 46 | - name: Inject Maps API Key 47 | env: 48 | MAPS_API_KEY: ${{ secrets.ACTIONS_API_KEY }} 49 | run: | 50 | [ -z "$MAPS_API_KEY" ] && MAPS_API_KEY="YOUR_API_KEY"; echo "MAPS_API_KEY=$MAPS_API_KEY" >> ./secrets.properties 51 | 52 | - name: Build debug 53 | run: ./gradlew assembleDebug 54 | 55 | - name: Run instrumentation tests 56 | uses: reactivecircus/android-emulator-runner@v2 57 | with: 58 | api-level: 29 59 | target: google_apis 60 | arch: x86 61 | disable-animations: true 62 | script: ./gradlew createDebugCoverageReport --stacktrace 63 | 64 | - name: Jacoco Report to PR 65 | id: jacoco 66 | uses: madrapps/jacoco-report@v1.7.1 67 | with: 68 | paths: | 69 | ${{ github.workspace }}/app/build/reports/coverage/androidTest/debug/connected/report.xml 70 | token: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} 71 | min-coverage-overall: 26 72 | min-coverage-changed-files: 60 73 | title: Code Coverage 74 | debug-mode: false 75 | update-comment: true 76 | 77 | - name: Get the Coverage info 78 | run: | 79 | echo "Total coverage ${{ steps.jacoco.outputs.coverage-overall }}" 80 | echo "Changed Files coverage ${{ steps.jacoco.outputs.coverage-changed-files }}" 81 | 82 | - name: Upload test reports 83 | if: always() 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: test-reports 87 | path: ./maps-app/build/reports 88 | -------------------------------------------------------------------------------- /.github/workflows/lint-report.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Lint and Upload SARIF 16 | 17 | on: 18 | pull_request: 19 | branches: 20 | - main 21 | jobs: 22 | lint: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v3 28 | 29 | - name: Set up JDK 17 30 | uses: actions/setup-java@v4.6.0 31 | with: 32 | distribution: 'adopt' 33 | java-version: '17' 34 | 35 | - name: Run Android Lint 36 | run: ./gradlew lint 37 | 38 | - name: Merge SARIF files 39 | run: | 40 | jq -s '{ "$schema": "https://json.schemastore.org/sarif-2.1.0", "version": "2.1.0", "runs": map(.runs) | add }' maps-compose/build/reports/lint-results.sarif maps-compose-utils/build/reports/lint-results.sarif maps-compose-widgets/build/reports/lint-results.sarif maps-app/build/reports/lint-results.sarif > merged.sarif 41 | 42 | - name: Upload SARIF file 43 | uses: github/codeql-action/upload-sarif@v3 44 | with: 45 | sarif_file: merged.sarif 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Release 16 | on: 17 | push: 18 | branches: [ main ] 19 | workflow_dispatch: 20 | 21 | jobs: 22 | release: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | token: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} 29 | - uses: gradle/actions/wrapper-validation@v4 30 | - name: Set up JDK 21 31 | uses: actions/setup-java@v4.6.0 32 | with: 33 | java-version: '21' 34 | distribution: 'adopt' 35 | - name: Create .gpg key 36 | run: | 37 | echo $GPG_KEY_ARMOR | base64 --decode > ./release.asc 38 | gpg --quiet --output $GITHUB_WORKSPACE/release.gpg --dearmor ./release.asc 39 | 40 | echo "Build and publish" 41 | sed -i -e "s,sonatypeToken=,sonatypeToken=$SONATYPE_TOKEN_USERNAME,g" gradle.properties 42 | SONATYPE_TOKEN_PASSWORD_ESCAPED=$(printf '%s\n' "$SONATYPE_TOKEN_PASSWORD" | sed -e 's/[\/&]/\\&/g') 43 | sed -i -e "s,sonatypeTokenPassword=,sonatypeTokenPassword=$SONATYPE_TOKEN_PASSWORD_ESCAPED,g" gradle.properties 44 | sed -i -e "s,signing.keyId=,signing.keyId=$GPG_KEY_ID,g" gradle.properties 45 | sed -i -e "s,signing.password=,signing.password=$GPG_PASSWORD,g" gradle.properties 46 | sed -i -e "s,signing.secretKeyRingFile=,signing.secretKeyRingFile=$GITHUB_WORKSPACE/release.gpg,g" gradle.properties 47 | env: 48 | GPG_KEY_ARMOR: "${{ secrets.SYNCED_GPG_KEY_ARMOR }}" 49 | GPG_KEY_ID: ${{ secrets.SYNCED_GPG_KEY_ID }} 50 | GPG_PASSWORD: ${{ secrets.SYNCED_GPG_KEY_PASSWORD }} 51 | SONATYPE_TOKEN_PASSWORD: ${{ secrets.SONATYPE_TOKEN_PASSWORD }} 52 | SONATYPE_TOKEN_USERNAME: ${{ secrets.SONATYPE_TOKEN }} 53 | 54 | - uses: actions/setup-node@v4 55 | with: 56 | node-version: '14' 57 | 58 | - name: Install conventionalcommits 59 | run: npm i -D conventional-changelog-conventionalcommits 60 | 61 | - name: Semantic Release 62 | uses: cycjimmy/semantic-release-action@v4.2.0 63 | with: 64 | extra_plugins: | 65 | "@semantic-release/commit-analyzer@8.0.1" 66 | "@semantic-release/release-notes-generator@9.0.3" 67 | "@google/semantic-release-replace-plugin@1.2.0" 68 | "@semantic-release/exec@5.0.0" 69 | "@semantic-release/git@9.0.1" 70 | "@semantic-release/github@7.2.3" 71 | env: 72 | GH_TOKEN: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} 73 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A workflow that runs tests on every new pull request 16 | name: Run unit tests 17 | 18 | on: 19 | repository_dispatch: 20 | types: [test] 21 | push: 22 | branches-ignore: ['gh-pages'] 23 | pull_request: 24 | branches-ignore: ['gh-pages'] 25 | workflow_dispatch: 26 | workflow_call: 27 | 28 | jobs: 29 | test: 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 34 | - name: Checkout Repo 35 | uses: actions/checkout@v4 36 | 37 | - name: Gradle Wrapper Validation 38 | uses: gradle/actions/wrapper-validation@v4 39 | 40 | - name: Set up JDK 17 41 | uses: actions/setup-java@v4.6.0 42 | with: 43 | java-version: '21' 44 | distribution: 'temurin' 45 | 46 | - name: Build modules 47 | run: ./gradlew build jacocoTestReport --stacktrace 48 | 49 | - name: Run Screenshot Tests 50 | run: ./gradlew validateDebugScreenshotTest 51 | 52 | - name: Upload build reports 53 | uses: actions/upload-artifact@v4 54 | if: always() 55 | with: 56 | name: my-artifact 57 | path: maps-app/build/reports 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | .gradle 4 | .kotlin 5 | /local.properties 6 | /.idea/caches 7 | /.idea/libraries 8 | /.idea/modules.xml 9 | /.idea/workspace.xml 10 | /.idea/navEditor.xml 11 | /.idea/assetWizardSettings.xml 12 | .DS_Store 13 | /build 14 | /captures 15 | .externalNativeBuild 16 | .cxx 17 | local.properties 18 | .java-version 19 | secrets.properties -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | branches: 2 | - main 3 | plugins: 4 | - "@semantic-release/commit-analyzer" 5 | - "@semantic-release/release-notes-generator" 6 | - - "@google/semantic-release-replace-plugin" 7 | - replacements: 8 | - files: 9 | - "build.gradle.kts" 10 | from: "\\bversion = \".*\"" 11 | to: "version = \"${nextRelease.version}\"" 12 | - files: 13 | - "README.md" 14 | from: ":([0-9]+).([0-9]+).([0-9]+)" 15 | to: ":${nextRelease.version}" 16 | - - "@semantic-release/exec" 17 | - prepareCmd: "./gradlew build --warn --stacktrace" 18 | publishCmd: "./gradlew publish --warn --stacktrace" 19 | - - "@semantic-release/git" 20 | - assets: 21 | - "build.gradle.kts" 22 | - "*.md" 23 | - "@semantic-release/github" 24 | options: 25 | debug: true 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Google Open Source Community Guidelines 2 | 3 | At Google, we recognize and celebrate the creativity and collaboration of open 4 | source contributors and the diversity of skills, experiences, cultures, and 5 | opinions they bring to the projects and communities they participate in. 6 | 7 | Every one of Google's open source projects and communities are inclusive 8 | environments, based on treating all individuals respectfully, regardless of 9 | gender identity and expression, sexual orientation, disabilities, 10 | neurodiversity, physical appearance, body size, ethnicity, nationality, race, 11 | age, religion, or similar personal characteristic. 12 | 13 | We value diverse opinions, but we value respectful behavior more. 14 | 15 | Respectful behavior includes: 16 | 17 | * Being considerate, kind, constructive, and helpful. 18 | * Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or 19 | physically threatening behavior, speech, and imagery. 20 | * Not engaging in unwanted physical contact. 21 | 22 | Some Google open source projects [may adopt][] an explicit project code of 23 | conduct, which may have additional detailed expectations for participants. Most 24 | of those projects will use our [modified Contributor Covenant][]. 25 | 26 | [may adopt]: https://opensource.google/docs/releasing/preparing/#conduct 27 | [modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ 28 | 29 | ## Resolve peacefully 30 | 31 | We do not believe that all conflict is necessarily bad; healthy debate and 32 | disagreement often yields positive results. However, it is never okay to be 33 | disrespectful. 34 | 35 | If you see someone behaving disrespectfully, you are encouraged to address the 36 | behavior directly with those involved. Many issues can be resolved quickly and 37 | easily, and this gives people more control over the outcome of their dispute. 38 | If you are unable to resolve the matter for any reason, or if the behavior is 39 | threatening or harassing, report it. We are dedicated to providing an 40 | environment where participants feel welcome and safe. 41 | 42 | ## Reporting problems 43 | 44 | Some Google open source projects may adopt a project-specific code of conduct. 45 | In those cases, a Google employee will be identified as the Project Steward, 46 | who will receive and handle reports of code of conduct violations. In the event 47 | that a project hasn’t identified a Project Steward, you can report problems by 48 | emailing opensource@google.com. 49 | 50 | We will investigate every complaint, but you may not receive a direct response. 51 | We will use our discretion in determining when and how to follow up on reported 52 | incidents, which may range from not taking action to permanent expulsion from 53 | the project and project-sponsored spaces. We will notify the accused of the 54 | report and provide them an opportunity to discuss it before any action is 55 | taken. The identity of the reporter will be omitted from the details of the 56 | report supplied to the accused. In potentially harmful situations, such as 57 | ongoing harassment or threats to anyone's safety, we may take action without 58 | notice. 59 | 60 | *This document was adapted from the [IndieWeb Code of Conduct][] and can also 61 | be found at .* 62 | 63 | [IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Compose API Guidelines 26 | 27 | Reviews will undergo strict enforcement of the [Jetpack Compose API guidelines](https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md). 28 | 29 | ## Community Guidelines 30 | 31 | This project follows 32 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 33 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Report a security issue 2 | 3 | To report a security issue, please use https://g.co/vulnz. We use 4 | https://g.co/vulnz for our intake, and do coordination and disclosure here on 5 | GitHub (including using GitHub Security Advisory). The Google Security Team will 6 | respond within 5 working days of your report on g.co/vulnz. 7 | 8 | To contact us about other bugs, please open an issue on GitHub. 9 | 10 | > **Note**: This file is synchronized from the https://github.com/googlemaps/.github repository. 11 | -------------------------------------------------------------------------------- /build-logic/.gitignore: -------------------------------------------------------------------------------- 1 | */build 2 | .gradle -------------------------------------------------------------------------------- /build-logic/convention/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | 11 | 12 | dependencies { 13 | implementation(libs.kotlin.gradle.plugin) 14 | implementation(libs.android.gradle.plugin) 15 | implementation(libs.dokka.plugin) 16 | implementation(libs.org.jacoco.core) 17 | } 18 | 19 | gradlePlugin { 20 | plugins { 21 | register("publishingConventionPlugin") { 22 | id = "android.maps.compose.PublishingConventionPlugin" 23 | implementationClass = "PublishingConventionPlugin" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /build-logic/convention/src/main/kotlin/PublishingConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | // buildSrc/src/main/kotlin/PublishingConventionPlugin.kt 2 | import org.gradle.api.Plugin 3 | import org.gradle.api.Project 4 | import org.gradle.api.publish.PublishingExtension 5 | import org.gradle.api.publish.maven.MavenPublication 6 | import org.gradle.kotlin.dsl.* 7 | import org.gradle.testing.jacoco.plugins.JacocoPluginExtension 8 | import org.gradle.api.tasks.testing.Test 9 | import org.gradle.testing.jacoco.plugins.JacocoTaskExtension 10 | import org.gradle.plugins.signing.SigningExtension 11 | import org.gradle.api.publish.maven.* 12 | 13 | class PublishingConventionPlugin : Plugin { 14 | override fun apply(project: Project) { 15 | project.run { 16 | 17 | applyPlugins() 18 | configureJacoco() 19 | configurePublishing() 20 | configureSigning() 21 | } 22 | } 23 | 24 | private fun Project.applyPlugins() { 25 | apply(plugin = "com.android.library") 26 | apply(plugin = "com.mxalbert.gradle.jacoco-android") 27 | apply(plugin = "maven-publish") 28 | apply(plugin = "org.jetbrains.dokka") 29 | apply(plugin = "signing") 30 | } 31 | 32 | private fun Project.configureJacoco() { 33 | configure { 34 | toolVersion = "0.8.7" 35 | 36 | } 37 | 38 | tasks.withType().configureEach { 39 | extensions.configure(JacocoTaskExtension::class.java) { 40 | isIncludeNoLocationClasses = true 41 | excludes = listOf("jdk.internal.*") 42 | } 43 | } 44 | } 45 | 46 | private fun Project.configurePublishing() { 47 | extensions.configure { 48 | publishing { 49 | singleVariant("release") { 50 | withSourcesJar() 51 | withJavadocJar() 52 | } 53 | } 54 | } 55 | extensions.configure { 56 | publications { 57 | create("aar") { 58 | afterEvaluate { 59 | from(components["release"]) 60 | } 61 | pom { 62 | name.set(project.name) 63 | description.set("Jetpack Compose components for the Maps SDK for Android") 64 | url.set("https://github.com/googlemaps/android-maps-compose") 65 | scm { 66 | connection.set("scm:git@github.com:googlemaps/android-maps-compose.git") 67 | developerConnection.set("scm:git@github.com:googlemaps/android-maps-compose.git") 68 | url.set("https://github.com/googlemaps/android-maps-compose") 69 | } 70 | licenses { 71 | license { 72 | name.set("The Apache Software License, Version 2.0") 73 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 74 | distribution.set("repo") 75 | } 76 | } 77 | organization { 78 | name.set("Google Inc") 79 | url.set("http://developers.google.com/maps") 80 | } 81 | developers { 82 | developer { 83 | name.set("Google Inc.") 84 | } 85 | } 86 | } 87 | } 88 | } 89 | repositories { 90 | maven { 91 | val releasesRepoUrl = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 92 | val snapshotsRepoUrl = uri("https://oss.sonatype.org/content/repositories/snapshots/") 93 | url = if (project.version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl 94 | credentials { 95 | username = project.findProperty("sonatypeToken") as String? 96 | password = project.findProperty("sonatypeTokenPassword") as String? 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | private fun Project.configureSigning() { 104 | configure { 105 | sign(extensions.getByType().publications["aar"]) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | versionCatalogs { 7 | create("libs") { 8 | from(files("../gradle/libs.versions.toml")) 9 | } 10 | } 11 | } 12 | 13 | rootProject.name = "build-logic" 14 | include(":convention") 15 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | val kotlinVersion by extra(libs.versions.kotlin.get()) 4 | val androidxTestVersion by extra(libs.versions.androidxtest.get()) 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath(libs.android.gradle.plugin) 11 | classpath(libs.maps.secrets.plugin) 12 | classpath(libs.kotlin.gradle.plugin) 13 | classpath(libs.dokka.plugin) 14 | classpath(libs.jacoco.android.plugin) 15 | } 16 | } 17 | 18 | plugins { 19 | alias(libs.plugins.dokka) apply true 20 | alias(libs.plugins.compose.compiler) apply false 21 | id("com.autonomousapps.dependency-analysis") version "2.0.0" 22 | alias(libs.plugins.android.application) apply false 23 | alias(libs.plugins.kotlin.android) apply false 24 | 25 | } 26 | 27 | val projectArtifactId by extra { project: Project -> 28 | if (project.name in listOf("maps-compose", "maps-compose-widgets", "maps-compose-utils")) { 29 | project.name 30 | } else { 31 | null 32 | } 33 | } 34 | 35 | allprojects { 36 | group = "com.google.maps.android" 37 | version = "6.6.0" 38 | val projectArtifactId by extra { project.name } 39 | } 40 | 41 | tasks.register("clean", Delete::class) { 42 | delete(rootProject.layout.buildDirectory) 43 | } 44 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | # (not needed, since no requested libraries are pre-AndroidX) 20 | #android.enableJetifier=false 21 | # This is needed for the navigation SDK library. It needs the recycler view. 22 | android.enableJetifier=true 23 | # Kotlin code style for this project: "official" or "obsolete": 24 | kotlin.code.style=official 25 | 26 | # variables required to allow build.gradle to parse, 27 | # override in ~/.gradle/gradle.properties 28 | signing.keyId= 29 | signing.password= 30 | signing.secretKeyRingFile= 31 | 32 | sonatypeToken= 33 | sonatypeTokenPassword= 34 | 35 | android.nonTransitiveRClass=false 36 | android.nonFinalResIds=false 37 | 38 | android.experimental.enableScreenshotTest=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | accompanistPermissions = "0.37.0" 3 | activitycompose = "1.10.1" 4 | agp = "8.9.1" 5 | androidCore = "1.6.1" 6 | androidx-core = "1.16.0" 7 | androidxtest = "1.6.2" 8 | compose-bom = "2025.04.00" 9 | dokka = "1.9.20" 10 | espresso = "3.6.1" 11 | jacoco-plugin = "0.2.1" 12 | junit = "4.13.2" 13 | junitVersion = "1.2.1" 14 | junitktx = "1.2.1" 15 | kotlin = "2.1.10" 16 | kotlinxCoroutines = "1.10.1" 17 | leakcanaryAndroid = "2.12" 18 | lifecycleRuntimeKtx = "2.8.7" 19 | mapsecrets = "2.0.1" 20 | mapsktx = "5.2.0" 21 | navigation = "6.2.0" 22 | org-jacoco-core = "0.8.12" 23 | places = "4.2.0" 24 | playServicesLocation = "21.3.0" 25 | robolectric = "4.14.1" 26 | screenshot = "0.0.1-alpha08" 27 | secretsGradlePlugin = "2.0.1" 28 | truth = "1.4.4" 29 | 30 | [libraries] 31 | # robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } 32 | accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } 33 | android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } 34 | androidx-compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activitycompose" } 35 | androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } 36 | androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } 37 | androidx-compose-material = { module = "androidx.compose.material:material" } 38 | androidx-compose-ui = { module = "androidx.compose.ui:ui" } 39 | androidx-compose-ui-preview-tooling = { module = "androidx.compose.ui:ui-tooling-preview" } 40 | androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } 41 | androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } 42 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 43 | androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" } 44 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } 45 | androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } 46 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" } 47 | androidx-test-compose-ui = { module = "androidx.compose.ui:ui-test-junit4" } 48 | androidx-test-core = { module = "androidx.test:core", version.ref = "androidCore" } 49 | androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } 50 | androidx-test-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitktx" } 51 | androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidCore" } 52 | androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxtest" } 53 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 54 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 55 | dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 56 | jacoco-android-plugin = { module = "com.mxalbert.gradle:jacoco-android", version.ref = "jacoco-plugin", version.require = "0.2.1" } 57 | kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" } 58 | kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 59 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } 60 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } 61 | leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" } 62 | maps-ktx-std = { module = "com.google.maps.android:maps-ktx", version.ref = "mapsktx" } 63 | maps-ktx-utils = { module = "com.google.maps.android:maps-utils-ktx", version.ref = "mapsktx" } 64 | maps-secrets-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "mapsecrets" } 65 | navigation = { module = "com.google.android.libraries.navigation:navigation", version.ref = "navigation" } 66 | org-jacoco-core = { module = "org.jacoco:org.jacoco.core", version.ref = "org-jacoco-core" } 67 | places = { group = "com.google.android.libraries.places", name = "places", version.ref = "places" } 68 | play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } 69 | test-junit = { module = "junit:junit", version.ref = "junit" } 70 | truth = { module = "com.google.truth:truth", version.ref = "truth" } 71 | 72 | [plugins] 73 | android-application = { id = "com.android.application", version.ref = "agp" } 74 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 75 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 76 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 77 | screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"} 78 | secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } 79 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /local.defaults.properties: -------------------------------------------------------------------------------- 1 | ## This file is automatically generated by Android Studio. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file should *NOT* be checked into Version Control Systems, 5 | # as it contains information specific to your local configuration. 6 | # 7 | # Location of the SDK. This is only used by Gradle. 8 | # For customization when using a Version Control System, please read the 9 | # header note. 10 | MAPS_API_KEY=YOUR_API_KEY 11 | PLACES_API_KEY=DEFAULT_API_KEY -------------------------------------------------------------------------------- /maps-app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /maps-app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") 5 | alias(libs.plugins.compose.compiler) 6 | alias(libs.plugins.screenshot) 7 | } 8 | 9 | android { 10 | lint { 11 | sarifOutput = file("$buildDir/reports/lint-results.sarif") 12 | } 13 | 14 | buildTypes { 15 | getByName("debug") { 16 | enableUnitTestCoverage = true 17 | enableAndroidTestCoverage = true 18 | } 19 | getByName("release") { 20 | enableUnitTestCoverage = true 21 | enableAndroidTestCoverage = true 22 | } 23 | } 24 | 25 | namespace = "com.google.maps.android.compose" 26 | compileSdk = 35 27 | 28 | defaultConfig { 29 | minSdk = 21 30 | targetSdk = 35 31 | versionCode = 1 32 | versionName = "1.0" 33 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 34 | } 35 | 36 | compileOptions { 37 | sourceCompatibility = JavaVersion.VERSION_1_8 38 | targetCompatibility = JavaVersion.VERSION_1_8 39 | } 40 | 41 | buildFeatures { 42 | buildConfig = true 43 | compose = true 44 | } 45 | 46 | kotlinOptions { 47 | jvmTarget = "1.8" 48 | freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" 49 | } 50 | 51 | experimentalProperties["android.experimental.enableScreenshotTest"] = true 52 | 53 | testOptions { 54 | screenshotTests { 55 | imageDifferenceThreshold = 0.035f // 3.5% 56 | } 57 | } 58 | } 59 | 60 | dependencies { 61 | implementation(platform(libs.androidx.compose.bom)) 62 | implementation(libs.androidx.compose.activity) 63 | implementation(libs.androidx.compose.foundation) 64 | implementation(libs.androidx.compose.material) 65 | implementation(libs.kotlin) 66 | implementation(libs.kotlinx.coroutines.android) 67 | implementation(libs.androidx.compose.ui.preview.tooling) 68 | debugImplementation(libs.androidx.compose.ui.tooling) 69 | debugImplementation(libs.leakcanary.android) 70 | 71 | 72 | androidTestImplementation(platform(libs.androidx.compose.bom)) 73 | androidTestImplementation(libs.androidx.test.core) 74 | androidTestImplementation(libs.androidx.test.rules) 75 | androidTestImplementation(libs.androidx.test.runner) 76 | androidTestImplementation(libs.androidx.test.espresso) 77 | androidTestImplementation(libs.androidx.test.junit.ktx) 78 | androidTestImplementation(libs.test.junit) 79 | androidTestImplementation(libs.androidx.test.compose.ui) 80 | androidTestImplementation(libs.kotlinx.coroutines.test) 81 | 82 | screenshotTestImplementation(libs.androidx.compose.ui.tooling) 83 | 84 | // Instead of the lines below, regular apps would load these libraries from Maven according to 85 | // the README installation instructions 86 | implementation(project(":maps-compose")) 87 | implementation(project(":maps-compose-widgets")) 88 | implementation(project(":maps-compose-utils")) 89 | } 90 | 91 | secrets { 92 | // To add your Maps API key to this project: 93 | // 1. If the secrets.properties file does not exist, create it in the same folder as the local.properties file. 94 | // 2. Add this line, where YOUR_API_KEY is your API key: 95 | // MAPS_API_KEY=YOUR_API_KEY 96 | propertiesFileName = "secrets.properties" 97 | 98 | // A properties file containing default secret values. This file can be 99 | // checked in version control. 100 | defaultPropertiesFileName = "local.defaults.properties" 101 | } 102 | -------------------------------------------------------------------------------- /maps-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /maps-app/src/androidTest/java/com/google/maps/android/compose/MapInColumnTests.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose 16 | 17 | import android.util.Log 18 | import androidx.compose.foundation.layout.fillMaxSize 19 | import androidx.compose.runtime.* 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.test.* 22 | import androidx.compose.ui.test.junit4.createComposeRule 23 | import com.google.android.gms.maps.model.CameraPosition 24 | import com.google.android.gms.maps.model.LatLng 25 | import org.junit.Assert.* 26 | import org.junit.Before 27 | import org.junit.Rule 28 | import org.junit.Test 29 | import java.util.concurrent.CountDownLatch 30 | import java.util.concurrent.TimeUnit 31 | 32 | private const val TAG = "MapInColumnTests" 33 | 34 | class MapInColumnTests { 35 | @get:Rule 36 | val composeTestRule = createComposeRule() 37 | 38 | private val startingZoom = 10f 39 | private val startingPosition = LatLng(1.23, 4.56) 40 | private lateinit var cameraPositionState: CameraPositionState 41 | 42 | private fun initMap() { 43 | check(hasValidApiKey) { "Maps API key not specified" } 44 | val countDownLatch = CountDownLatch(1) 45 | composeTestRule.setContent { 46 | var scrollingEnabled by remember { mutableStateOf(true) } 47 | 48 | LaunchedEffect(cameraPositionState.isMoving) { 49 | if (!cameraPositionState.isMoving) { 50 | scrollingEnabled = true 51 | Log.d(TAG, "Map camera stopped moving - Enabling column scrolling...") 52 | } 53 | } 54 | 55 | MapInColumn( 56 | modifier = Modifier.fillMaxSize(), 57 | cameraPositionState, 58 | columnScrollingEnabled = scrollingEnabled, 59 | onMapTouched = { 60 | scrollingEnabled = false 61 | Log.d( 62 | TAG, 63 | "User touched map - Disabling column scrolling after user touched this Box..." 64 | ) 65 | }, 66 | onMapLoaded = { 67 | countDownLatch.countDown() 68 | } 69 | ) 70 | } 71 | val mapLoaded = countDownLatch.await(30, TimeUnit.SECONDS) 72 | assertTrue("Map loaded", mapLoaded) 73 | } 74 | 75 | @Before 76 | fun setUp() { 77 | cameraPositionState = CameraPositionState( 78 | position = CameraPosition.fromLatLngZoom( 79 | startingPosition, 80 | startingZoom 81 | ) 82 | ) 83 | } 84 | 85 | @Test 86 | fun testStartingCameraPosition() { 87 | initMap() 88 | startingPosition.assertEquals(cameraPositionState.position.target) 89 | } 90 | 91 | @Test 92 | fun testLatLngInVisibleRegion() { 93 | initMap() 94 | composeTestRule.runOnUiThread { 95 | val projection = cameraPositionState.projection 96 | assertNotNull(projection) 97 | assertTrue( 98 | projection!!.visibleRegion.latLngBounds.contains(startingPosition) 99 | ) 100 | } 101 | } 102 | 103 | @Test 104 | fun testLatLngNotInVisibleRegion() { 105 | initMap() 106 | composeTestRule.runOnUiThread { 107 | val projection = cameraPositionState.projection 108 | assertNotNull(projection) 109 | val latLng = LatLng(23.4, 25.6) 110 | assertFalse( 111 | projection!!.visibleRegion.latLngBounds.contains(latLng) 112 | ) 113 | } 114 | } 115 | 116 | @Test 117 | fun testScrollColumn_MapCameraRemainsSame() { 118 | initMap() 119 | // Check that the column scrolls to the last item 120 | composeTestRule.onRoot().performTouchInput { 121 | swipeUp( 122 | startY = (bottom - top) / 2 123 | ) 124 | } 125 | 126 | composeTestRule.waitForIdle() 127 | // composeTestRule.onNodeWithTag("Item 1").assertIsNotDisplayed() 128 | 129 | // Check that the map didn't change 130 | startingPosition.assertEquals(cameraPositionState.position.target) 131 | } 132 | 133 | @Test 134 | fun testPanMapUp_MapCameraChangesColumnDoesNotScroll() { 135 | initMap() 136 | //Swipe the map up 137 | composeTestRule.onAllNodesWithTag("Map").onFirst().performTouchInput { swipeUp() } 138 | composeTestRule.waitForIdle() 139 | 140 | //Make sure that the map changed (i.e., we can scroll the map in the column) 141 | assertNotEquals(startingPosition, cameraPositionState.position.target) 142 | 143 | //Check to make sure column didn't scroll 144 | composeTestRule.onNodeWithTag("Item 1").assertIsDisplayed() 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /maps-app/src/androidTest/java/com/google/maps/android/compose/MapsInLazyColumnTest.kt: -------------------------------------------------------------------------------- 1 | package com.google.maps.android.compose 2 | 3 | import androidx.compose.foundation.lazy.rememberLazyListState 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.derivedStateOf 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.test.assertIsDisplayed 10 | import androidx.compose.ui.test.junit4.createComposeRule 11 | import androidx.compose.ui.test.onNodeWithTag 12 | import androidx.compose.ui.test.onRoot 13 | import androidx.compose.ui.test.performTouchInput 14 | import androidx.compose.ui.test.swipeUp 15 | import com.google.android.gms.maps.model.CameraPosition 16 | import com.google.android.gms.maps.model.LatLng 17 | import org.junit.Assert.assertTrue 18 | import org.junit.Before 19 | import org.junit.Rule 20 | import org.junit.Test 21 | import java.util.concurrent.CountDownLatch 22 | import java.util.concurrent.TimeUnit 23 | 24 | class MapsInLazyColumnTests { 25 | @get:Rule 26 | val composeTestRule = createComposeRule() 27 | 28 | private val mapItems = listOf( 29 | MapListItem(id = "1", location = LatLng(1.23, 4.56), zoom = 10f, title = "Item 1"), 30 | MapListItem(id = "2", location = LatLng(7.89, 0.12), zoom = 12f, title = "Item 2"), 31 | MapListItem(id = "3", location = LatLng(3.45, 6.78), zoom = 11f, title = "Item 3"), 32 | MapListItem(id = "4", location = LatLng(9.01, 2.34), zoom = 13f, title = "Item 4"), 33 | MapListItem(id = "5", location = LatLng(5.67, 8.90), zoom = 9f, title = "Item 5"), 34 | MapListItem(id = "6", location = LatLng(4.32, 7.65), zoom = 14f, title = "Item 6"), 35 | MapListItem(id = "7", location = LatLng(8.76, 1.23), zoom = 10f, title = "Item 7"), 36 | MapListItem(id = "8", location = LatLng(2.98, 6.54), zoom = 12f, title = "Item 8"), 37 | MapListItem(id = "9", location = LatLng(7.65, 3.21), zoom = 11f, title = "Item 9"), 38 | MapListItem(id = "10", location = LatLng(0.12, 9.87), zoom = 13f, title = "Item 10"), 39 | ) 40 | 41 | 42 | private lateinit var cameraPositionStates: Map 43 | 44 | private fun initMaps() { 45 | check(hasValidApiKey) { "Maps API key not specified" } 46 | 47 | composeTestRule.setContent { 48 | val lazyListState = rememberLazyListState() 49 | val visibleMapCount = remember { mutableStateOf(0) } 50 | 51 | val visibleItems by remember { 52 | derivedStateOf { 53 | lazyListState.layoutInfo.visibleItemsInfo.size 54 | } 55 | } 56 | 57 | LaunchedEffect(visibleItems) { 58 | visibleMapCount.value = visibleItems 59 | } 60 | 61 | val countDownLatch = CountDownLatch(visibleMapCount.value) 62 | 63 | MapsInLazyColumn( 64 | mapItems, 65 | lazyListState = lazyListState, 66 | onMapLoaded = { 67 | countDownLatch.countDown() 68 | } 69 | ) 70 | 71 | LaunchedEffect(Unit) { 72 | val mapsLoaded = countDownLatch.await(30, TimeUnit.SECONDS) 73 | assertTrue("Visible maps loaded", mapsLoaded) 74 | } 75 | } 76 | } 77 | 78 | @Before 79 | fun setUp() { 80 | cameraPositionStates = mapItems.associate { item -> 81 | item.id to CameraPositionState( 82 | position = CameraPosition.fromLatLngZoom(item.location, item.zoom) 83 | ) 84 | } 85 | } 86 | 87 | @Test 88 | fun testStartingCameraPositions() { 89 | initMaps() 90 | mapItems.forEach { item -> 91 | item.location.assertEquals(cameraPositionStates[item.id]?.position?.target!!) 92 | } 93 | } 94 | 95 | @Test 96 | fun testLazyColumnScrolls_MapPositionsRemain() { 97 | initMaps() 98 | composeTestRule.onRoot().performTouchInput { swipeUp() } 99 | composeTestRule.waitForIdle() 100 | 101 | mapItems.forEach { item -> 102 | item.location.assertEquals(cameraPositionStates[item.id]?.position?.target!!) 103 | } 104 | } 105 | 106 | @Test 107 | fun testScrollToBottom() { 108 | initMaps() 109 | composeTestRule.onRoot().performTouchInput { swipeUp(durationMillis = 1000) } 110 | composeTestRule.waitForIdle() 111 | //We do not need to check anything on the test, just to make sure the scroll down doesnt crash 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /maps-app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt: -------------------------------------------------------------------------------- 1 | package com.google.maps.android.compose 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.semantics.contentDescription 5 | import androidx.compose.ui.semantics.semantics 6 | import androidx.compose.ui.test.junit4.createComposeRule 7 | import com.google.android.gms.maps.StreetViewPanoramaOptions 8 | import com.google.android.gms.maps.model.StreetViewPanoramaOrientation 9 | import com.google.maps.android.compose.streetview.StreetView 10 | import com.google.maps.android.compose.streetview.StreetViewCameraPositionState 11 | import com.google.maps.android.ktx.MapsExperimentalFeature 12 | import org.junit.Before 13 | import org.junit.Rule 14 | import org.junit.Test 15 | 16 | class StreetViewTests { 17 | @get:Rule 18 | val composeTestRule = createComposeRule() 19 | 20 | private lateinit var cameraPositionState: StreetViewCameraPositionState 21 | private val initialLatLng = singapore 22 | 23 | @Before 24 | fun setUp() { 25 | cameraPositionState = StreetViewCameraPositionState() 26 | } 27 | 28 | @OptIn(MapsExperimentalFeature::class) 29 | private fun initStreetView(onClick: (StreetViewPanoramaOrientation) -> Unit = {}) { 30 | composeTestRule.setContent { 31 | StreetView( 32 | Modifier.semantics { contentDescription = "StreetView" }, 33 | cameraPositionState = cameraPositionState, 34 | streetViewPanoramaOptionsFactory = { 35 | StreetViewPanoramaOptions() 36 | .position(initialLatLng) 37 | }, 38 | onClick = onClick 39 | ) 40 | } 41 | composeTestRule.waitUntil(timeout5) { 42 | cameraPositionState.location.position.latitude != 0.0 && 43 | cameraPositionState.location.position.longitude != 0.0 44 | } 45 | } 46 | 47 | @Test 48 | fun testStartingStreetViewPosition() { 49 | initStreetView() 50 | initialLatLng.assertEquals(cameraPositionState.location.position) 51 | } 52 | } -------------------------------------------------------------------------------- /maps-app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.google.maps.android.compose 2 | 3 | import com.google.android.gms.maps.model.LatLng 4 | import org.junit.Assert.assertEquals 5 | const val timeout2 = 2_000L 6 | const val timeout3 = 3_000L 7 | const val timeout5 = 5_000L 8 | 9 | val hasValidApiKey: Boolean = 10 | BuildConfig.MAPS_API_KEY.isNotBlank() && BuildConfig.MAPS_API_KEY != "YOUR_API_KEY" 11 | 12 | const val assertRoundingError: Double = 0.01 13 | 14 | fun LatLng.assertEquals(other: LatLng) { 15 | assertEquals(latitude, other.latitude, assertRoundingError) 16 | assertEquals(longitude, other.longitude, assertRoundingError) 17 | } 18 | 19 | 20 | fun ComposeMapColorScheme.assertEquals(other: ComposeMapColorScheme) { 21 | assertEquals(other, this) 22 | } -------------------------------------------------------------------------------- /maps-app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewDisappearingScaleBar_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewDisappearingScaleBar_0.png -------------------------------------------------------------------------------- /maps-app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewScaleBar_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/debug/screenshotTest/reference/com/google/maps/android/compose/ScaleBarTest/PreviewScaleBar_0.png -------------------------------------------------------------------------------- /maps-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | 63 | 66 | 69 | 72 | 75 | 78 | 81 | 84 | 87 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/AccessibilityActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose 16 | 17 | import android.os.Bundle 18 | import android.util.Log 19 | import androidx.activity.ComponentActivity 20 | import androidx.activity.compose.setContent 21 | import androidx.activity.enableEdgeToEdge 22 | import androidx.compose.foundation.layout.Box 23 | import androidx.compose.foundation.layout.fillMaxSize 24 | import androidx.compose.foundation.layout.systemBarsPadding 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.ui.Modifier 29 | import com.google.android.gms.maps.model.Marker 30 | 31 | private const val TAG = "AccessibilityActivity" 32 | 33 | 34 | class AccessibilityActivity : ComponentActivity() { 35 | 36 | override fun onCreate(savedInstanceState: Bundle?) { 37 | super.onCreate(savedInstanceState) 38 | enableEdgeToEdge() 39 | setContent { 40 | val singaporeState = rememberMarkerState(position = singapore) 41 | val cameraPositionState = rememberCameraPositionState { 42 | position = defaultCameraPosition 43 | } 44 | val uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) } 45 | val mapProperties by remember { 46 | mutableStateOf(MapProperties(mapType = MapType.NORMAL)) 47 | } 48 | 49 | Box( 50 | modifier = Modifier.fillMaxSize() 51 | .systemBarsPadding(), 52 | ) { 53 | GoogleMap( 54 | // mergeDescendants will remove accessibility from the entire map and content inside. 55 | mergeDescendants = true, 56 | // alternatively, contentDescription will deactivate it for the maps, but not markers. 57 | contentDescription = "", 58 | cameraPositionState = cameraPositionState, 59 | properties = mapProperties, 60 | uiSettings = uiSettings, 61 | onPOIClick = { 62 | Log.d(TAG, "POI clicked: ${it.name}") 63 | } 64 | ) { 65 | val markerClick: (Marker) -> Boolean = { 66 | Log.d(TAG, "${it.title} was clicked") 67 | cameraPositionState.projection?.let { projection -> 68 | Log.d(TAG, "The current projection is: $projection") 69 | } 70 | false 71 | } 72 | 73 | Marker( 74 | // contentDescription overrides title for TalkBack 75 | contentDescription = "Description of the marker", 76 | state = singaporeState, 77 | title = "Marker in Singapore", 78 | onClick = markerClick 79 | ) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/CustomControlsActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose 16 | 17 | import android.os.Bundle 18 | import android.widget.Toast 19 | import androidx.activity.ComponentActivity 20 | import androidx.activity.compose.setContent 21 | import androidx.activity.enableEdgeToEdge 22 | import androidx.compose.animation.AnimatedVisibility 23 | import androidx.compose.animation.EnterTransition 24 | import androidx.compose.animation.fadeOut 25 | import androidx.compose.foundation.background 26 | import androidx.compose.foundation.layout.Box 27 | import androidx.compose.foundation.layout.Column 28 | import androidx.compose.foundation.layout.fillMaxSize 29 | import androidx.compose.foundation.layout.fillMaxWidth 30 | import androidx.compose.foundation.layout.padding 31 | import androidx.compose.foundation.layout.systemBarsPadding 32 | import androidx.compose.foundation.layout.wrapContentSize 33 | import androidx.compose.material.Button 34 | import androidx.compose.material.ButtonDefaults 35 | import androidx.compose.material.CircularProgressIndicator 36 | import androidx.compose.material.MaterialTheme 37 | import androidx.compose.material.Text 38 | import androidx.compose.runtime.Composable 39 | import androidx.compose.runtime.getValue 40 | import androidx.compose.runtime.mutableStateOf 41 | import androidx.compose.runtime.remember 42 | import androidx.compose.runtime.rememberCoroutineScope 43 | import androidx.compose.runtime.setValue 44 | import androidx.compose.ui.Modifier 45 | import androidx.compose.ui.unit.dp 46 | import com.google.android.gms.maps.CameraUpdateFactory 47 | import kotlinx.coroutines.launch 48 | 49 | 50 | class CustomControlsActivity : ComponentActivity() { 51 | 52 | override fun onCreate(savedInstanceState: Bundle?) { 53 | super.onCreate(savedInstanceState) 54 | enableEdgeToEdge() 55 | setContent { 56 | var isMapLoaded by remember { mutableStateOf(false) } 57 | val coroutineScope = rememberCoroutineScope() 58 | // This needs to be manually deactivated to avoid having a custom and the native 59 | // location button 60 | val uiSettings by remember { 61 | mutableStateOf( 62 | MapUiSettings( 63 | myLocationButtonEnabled = false, 64 | zoomGesturesEnabled = false 65 | ) 66 | ) 67 | } 68 | // Observing and controlling the camera's state can be done with a CameraPositionState 69 | val cameraPositionState = rememberCameraPositionState { 70 | position = defaultCameraPosition 71 | } 72 | 73 | Box( 74 | modifier = Modifier.fillMaxSize() 75 | .systemBarsPadding(), 76 | ) { 77 | GoogleMap( 78 | modifier = Modifier.matchParentSize(), 79 | cameraPositionState = cameraPositionState, 80 | onMapLoaded = { 81 | isMapLoaded = true 82 | }, 83 | uiSettings = uiSettings, 84 | ) 85 | 86 | if (!isMapLoaded) { 87 | AnimatedVisibility( 88 | modifier = Modifier 89 | .matchParentSize(), 90 | visible = !isMapLoaded, 91 | enter = EnterTransition.None, 92 | exit = fadeOut() 93 | ) { 94 | CircularProgressIndicator( 95 | modifier = Modifier 96 | .background(MaterialTheme.colors.background) 97 | .wrapContentSize() 98 | ) 99 | } 100 | } 101 | Column( 102 | modifier = Modifier.fillMaxWidth() 103 | ) { 104 | MapButton( 105 | "This is a custom location button", 106 | onClick = { 107 | Toast.makeText( 108 | this@CustomControlsActivity, 109 | "Click on my location", 110 | Toast.LENGTH_SHORT 111 | ).show() 112 | }) 113 | MapButton( 114 | "+", 115 | onClick = { 116 | coroutineScope.launch { 117 | cameraPositionState.animate(CameraUpdateFactory.zoomIn()) 118 | } 119 | }) 120 | MapButton( 121 | "-", 122 | onClick = { 123 | coroutineScope.launch { 124 | cameraPositionState.animate(CameraUpdateFactory.zoomOut()) 125 | } 126 | }) 127 | } 128 | } 129 | } 130 | } 131 | 132 | @Composable 133 | private fun MapButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { 134 | Button( 135 | modifier = modifier.padding(4.dp), 136 | colors = ButtonDefaults.buttonColors( 137 | backgroundColor = MaterialTheme.colors.onPrimary, 138 | contentColor = MaterialTheme.colors.primary 139 | ), 140 | onClick = onClick 141 | ) { 142 | Text(text = text, style = MaterialTheme.typography.body1) 143 | } 144 | } 145 | 146 | 147 | } 148 | -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/RecompositionActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose 16 | 17 | import android.os.Bundle 18 | import android.util.Log 19 | import androidx.activity.ComponentActivity 20 | import androidx.activity.compose.setContent 21 | import androidx.activity.enableEdgeToEdge 22 | import androidx.compose.foundation.layout.Box 23 | import androidx.compose.foundation.layout.Column 24 | import androidx.compose.foundation.layout.fillMaxSize 25 | import androidx.compose.foundation.layout.systemBarsPadding 26 | import androidx.compose.material.Button 27 | import androidx.compose.material.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.runtime.mutableStateOf 31 | import androidx.compose.runtime.remember 32 | import androidx.compose.ui.Modifier 33 | import com.google.android.gms.maps.model.Marker 34 | import com.google.maps.android.compose.theme.MapsComposeSampleTheme 35 | import kotlin.random.Random 36 | 37 | private const val TAG = "RecompositionActivity" 38 | 39 | /** 40 | * This is a sample activity showcasing how the recomposition works. The location is changed 41 | * every time we click on the button, and the marker gets updated (removed and added in a new 42 | * location) 43 | */ 44 | class RecompositionActivity : ComponentActivity() { 45 | 46 | override fun onCreate(savedInstanceState: Bundle?) { 47 | super.onCreate(savedInstanceState) 48 | enableEdgeToEdge() 49 | setContent { 50 | val cameraPositionState = rememberCameraPositionState { 51 | position = defaultCameraPosition 52 | } 53 | Box( 54 | modifier = Modifier.fillMaxSize() 55 | .systemBarsPadding(), 56 | ) { 57 | MapsComposeSampleTheme { 58 | GoogleMapView( 59 | modifier = Modifier.matchParentSize(), 60 | cameraPositionState = cameraPositionState 61 | ) 62 | } 63 | } 64 | } 65 | } 66 | 67 | @Composable 68 | fun GoogleMapView( 69 | modifier: Modifier = Modifier, 70 | cameraPositionState: CameraPositionState = rememberCameraPositionState(), 71 | content: @Composable () -> Unit = {}, 72 | ) { 73 | val markerState = rememberMarkerState(position = singapore) 74 | 75 | val uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) } 76 | val mapProperties by remember { 77 | mutableStateOf(MapProperties(mapType = MapType.NORMAL)) 78 | } 79 | 80 | val mapVisible by remember { mutableStateOf(true) } 81 | if (mapVisible) { 82 | GoogleMap( 83 | modifier = modifier, 84 | cameraPositionState = cameraPositionState, 85 | properties = mapProperties, 86 | uiSettings = uiSettings, 87 | onPOIClick = { 88 | Log.d(TAG, "POI clicked: ${it.name}") 89 | } 90 | ) { 91 | val markerClick: (Marker) -> Boolean = { 92 | Log.d(TAG, "${it.title} was clicked") 93 | cameraPositionState.projection?.let { projection -> 94 | Log.d(TAG, "The current projection is: $projection") 95 | } 96 | false 97 | } 98 | 99 | Marker( 100 | state = markerState, 101 | title = "Marker in Singapore", 102 | onClick = markerClick 103 | ) 104 | 105 | content() 106 | } 107 | Column { 108 | Button(onClick = { 109 | val randomValue = Random.nextInt(3) 110 | markerState.position = when (randomValue) { 111 | 0 -> singapore 112 | 1 -> singapore2 113 | 2 -> singapore3 114 | else -> singapore 115 | } 116 | }) { 117 | Text("Change Location") 118 | } 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/ScaleBarActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose 16 | 17 | import android.os.Bundle 18 | import androidx.activity.ComponentActivity 19 | import androidx.activity.compose.setContent 20 | import androidx.activity.enableEdgeToEdge 21 | import androidx.compose.animation.AnimatedVisibility 22 | import androidx.compose.animation.EnterTransition 23 | import androidx.compose.animation.fadeOut 24 | import androidx.compose.foundation.BorderStroke 25 | import androidx.compose.foundation.background 26 | import androidx.compose.foundation.border 27 | import androidx.compose.foundation.layout.Box 28 | import androidx.compose.foundation.layout.fillMaxSize 29 | import androidx.compose.foundation.layout.padding 30 | import androidx.compose.foundation.layout.systemBarsPadding 31 | import androidx.compose.foundation.layout.wrapContentSize 32 | import androidx.compose.material.CircularProgressIndicator 33 | import androidx.compose.material.MaterialTheme 34 | import androidx.compose.runtime.Composable 35 | import androidx.compose.runtime.getValue 36 | import androidx.compose.runtime.mutableStateOf 37 | import androidx.compose.runtime.remember 38 | import androidx.compose.runtime.setValue 39 | import androidx.compose.ui.Alignment 40 | import androidx.compose.ui.Modifier 41 | import androidx.compose.ui.tooling.preview.Preview 42 | import androidx.compose.ui.unit.dp 43 | import com.google.android.gms.maps.model.CameraPosition 44 | import com.google.android.gms.maps.model.LatLng 45 | import com.google.maps.android.compose.theme.MapsComposeSampleTheme 46 | import com.google.maps.android.compose.widgets.DarkGray 47 | import com.google.maps.android.compose.widgets.DisappearingScaleBar 48 | import com.google.maps.android.compose.widgets.ScaleBar 49 | 50 | class ScaleBarActivity : ComponentActivity() { 51 | 52 | override fun onCreate(savedInstanceState: Bundle?) { 53 | super.onCreate(savedInstanceState) 54 | enableEdgeToEdge() 55 | setContent { 56 | var isMapLoaded by remember { mutableStateOf(false) } 57 | 58 | // To control and observe the map camera 59 | val cameraPositionState = rememberCameraPositionState { 60 | position = defaultCameraPosition 61 | } 62 | 63 | val scaleBackground = MaterialTheme.colors.background.copy(alpha = 0.4f) 64 | val scaleBorderStroke = BorderStroke(width = 1.dp, DarkGray.copy(alpha = 0.2f)) 65 | 66 | Box( 67 | modifier = Modifier.fillMaxSize() 68 | .systemBarsPadding(), 69 | ) { 70 | GoogleMap( 71 | modifier = Modifier.matchParentSize(), 72 | cameraPositionState = cameraPositionState, 73 | onMapLoaded = { 74 | isMapLoaded = true 75 | } 76 | ) 77 | 78 | Box( 79 | modifier = Modifier 80 | .padding(top = 5.dp, start = 5.dp) 81 | .align(Alignment.TopStart) 82 | .background( 83 | scaleBackground, 84 | shape = MaterialTheme.shapes.medium 85 | ) 86 | .border( 87 | scaleBorderStroke, 88 | shape = MaterialTheme.shapes.medium 89 | ), 90 | ) { 91 | DisappearingScaleBar( 92 | modifier = Modifier.padding(end = 4.dp), 93 | cameraPositionState = cameraPositionState 94 | ) 95 | } 96 | 97 | Box( 98 | modifier = Modifier 99 | .padding(top = 5.dp, end = 5.dp) 100 | .align(Alignment.TopEnd) 101 | .background( 102 | scaleBackground, 103 | shape = MaterialTheme.shapes.medium, 104 | ) 105 | .border( 106 | scaleBorderStroke, 107 | shape = MaterialTheme.shapes.medium 108 | ), 109 | ) { 110 | ScaleBar( 111 | modifier = Modifier.padding(end = 4.dp), 112 | cameraPositionState = cameraPositionState 113 | ) 114 | 115 | } 116 | if (!isMapLoaded) { 117 | AnimatedVisibility( 118 | modifier = Modifier 119 | .matchParentSize(), 120 | visible = !isMapLoaded, 121 | enter = EnterTransition.None, 122 | exit = fadeOut() 123 | ) { 124 | CircularProgressIndicator( 125 | modifier = Modifier 126 | .background(MaterialTheme.colors.background) 127 | .wrapContentSize() 128 | ) 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | @Preview 137 | @Composable 138 | fun PreviewScaleBar() { 139 | val cameraPositionState = remember { 140 | CameraPositionState( 141 | position = CameraPosition( 142 | LatLng(48.137154, 11.576124), // Example coordinates: Munich, Germany 143 | 12f, 144 | 0f, 145 | 0f 146 | ) 147 | ) 148 | } 149 | 150 | MapsComposeSampleTheme { 151 | ScaleBar( 152 | modifier = Modifier.padding(end = 4.dp), 153 | cameraPositionState = cameraPositionState 154 | ) 155 | } 156 | } -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose 16 | 17 | import android.os.Bundle 18 | import android.util.Log 19 | import androidx.activity.ComponentActivity 20 | import androidx.activity.compose.setContent 21 | import androidx.activity.enableEdgeToEdge 22 | import androidx.compose.foundation.background 23 | import androidx.compose.foundation.layout.Box 24 | import androidx.compose.foundation.layout.Column 25 | import androidx.compose.foundation.layout.Row 26 | import androidx.compose.foundation.layout.Spacer 27 | import androidx.compose.foundation.layout.fillMaxSize 28 | import androidx.compose.foundation.layout.fillMaxWidth 29 | import androidx.compose.foundation.layout.padding 30 | import androidx.compose.foundation.layout.systemBarsPadding 31 | import androidx.compose.material.Switch 32 | import androidx.compose.material.Text 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.runtime.LaunchedEffect 35 | import androidx.compose.runtime.getValue 36 | import androidx.compose.runtime.mutableStateOf 37 | import androidx.compose.runtime.remember 38 | import androidx.compose.runtime.setValue 39 | import androidx.compose.runtime.snapshotFlow 40 | import androidx.compose.ui.Alignment 41 | import androidx.compose.ui.Modifier 42 | import androidx.compose.ui.graphics.Color 43 | import androidx.compose.ui.unit.dp 44 | import com.google.android.gms.maps.StreetViewPanoramaOptions 45 | import com.google.android.gms.maps.model.LatLng 46 | import com.google.maps.android.Status 47 | import com.google.maps.android.compose.streetview.StreetView 48 | import com.google.maps.android.compose.streetview.rememberStreetViewCameraPositionState 49 | import com.google.maps.android.ktx.MapsExperimentalFeature 50 | import kotlinx.coroutines.launch 51 | import com.google.maps.android.StreetViewUtils.Companion.fetchStreetViewData 52 | 53 | class StreetViewActivity : ComponentActivity() { 54 | 55 | private val TAG = StreetViewActivity::class.java.simpleName 56 | 57 | // This is an invalid location. If you use it instead of Singapore, the StreetViewUtils 58 | // will return NOT_FOUND. 59 | val invalidLocation = LatLng(32.429634, -96.828891) 60 | 61 | @OptIn(MapsExperimentalFeature::class) 62 | override fun onCreate(savedInstanceState: Bundle?) { 63 | super.onCreate(savedInstanceState) 64 | enableEdgeToEdge() 65 | setContent { 66 | var isPanningEnabled by remember { mutableStateOf(false) } 67 | var isZoomEnabled by remember { mutableStateOf(false) } 68 | var streetViewResult by remember { mutableStateOf(Status.NOT_FOUND) } 69 | 70 | val camera = rememberStreetViewCameraPositionState() 71 | LaunchedEffect(camera) { 72 | launch { 73 | snapshotFlow { camera.panoramaCamera } 74 | .collect { 75 | Log.d(TAG, "Camera at: $it") 76 | } 77 | } 78 | launch { 79 | snapshotFlow { camera.location } 80 | .collect { 81 | Log.d(TAG, "Location at: $it") 82 | } 83 | } 84 | launch { 85 | // Be sure to enable the Street View Static API on the project associated with 86 | // this API key using the instructions at https://goo.gle/enable-sv-static-api 87 | streetViewResult = 88 | fetchStreetViewData(singapore, BuildConfig.MAPS_API_KEY) 89 | } 90 | } 91 | Box( 92 | modifier = Modifier.fillMaxSize() 93 | .systemBarsPadding(), 94 | contentAlignment = Alignment.BottomStart, 95 | ) { 96 | if (streetViewResult == Status.OK) { 97 | StreetView( 98 | Modifier.matchParentSize(), 99 | cameraPositionState = camera, 100 | streetViewPanoramaOptionsFactory = { 101 | StreetViewPanoramaOptions().position(singapore) 102 | }, 103 | isPanningGesturesEnabled = isPanningEnabled, 104 | isZoomGesturesEnabled = isZoomEnabled, 105 | onClick = { 106 | Log.d(TAG, "Street view clicked") 107 | }, 108 | onLongClick = { 109 | Log.d(TAG, "Street view long clicked") 110 | } 111 | ) 112 | Column( 113 | Modifier 114 | .fillMaxWidth() 115 | .background(Color.White) 116 | .padding(8.dp) 117 | ) { 118 | StreetViewSwitch(title = "Panning", checked = isPanningEnabled) { 119 | isPanningEnabled = it 120 | } 121 | StreetViewSwitch(title = "Zooming", checked = isZoomEnabled) { 122 | isZoomEnabled = it 123 | } 124 | } 125 | } else { 126 | Text("Location not available.") 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | 134 | @Composable 135 | fun StreetViewSwitch(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { 136 | Row(Modifier.padding(4.dp)) { 137 | Text(title) 138 | Spacer(Modifier.weight(1f)) 139 | Switch(checked = checked, onCheckedChange = onCheckedChange) 140 | } 141 | } -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose.markerexamples.markerdragevents 16 | 17 | import android.os.Bundle 18 | import android.util.Log 19 | import androidx.activity.ComponentActivity 20 | import androidx.activity.compose.setContent 21 | import androidx.activity.enableEdgeToEdge 22 | import androidx.compose.foundation.layout.fillMaxSize 23 | import androidx.compose.foundation.layout.systemBarsPadding 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.LaunchedEffect 26 | import androidx.compose.runtime.snapshotFlow 27 | import androidx.compose.ui.Modifier 28 | import com.google.android.gms.maps.model.LatLng 29 | import com.google.maps.android.compose.GoogleMap 30 | import com.google.maps.android.compose.Marker 31 | import com.google.maps.android.compose.defaultCameraPosition 32 | import com.google.maps.android.compose.rememberCameraPositionState 33 | import com.google.maps.android.compose.rememberMarkerState 34 | import com.google.maps.android.compose.singapore 35 | import com.google.maps.android.compose.theme.MapsComposeSampleTheme 36 | import kotlinx.coroutines.flow.dropWhile 37 | 38 | private val TAG = MarkerDragEventsActivity::class.simpleName 39 | 40 | /** 41 | * Demonstrates how to reliably generate a sequence of Marker drag START-DRAG-END events as in the 42 | * original GoogleMap Marker listener. 43 | */ 44 | class MarkerDragEventsActivity : ComponentActivity() { 45 | override fun onCreate(savedInstanceState: Bundle?) { 46 | super.onCreate(savedInstanceState) 47 | enableEdgeToEdge() 48 | setContent { 49 | MapsComposeSampleTheme { 50 | GoogleMapWithMarker( 51 | modifier = Modifier.fillMaxSize() 52 | .systemBarsPadding(), 53 | ) 54 | } 55 | } 56 | } 57 | } 58 | 59 | @Composable 60 | private fun GoogleMapWithMarker( 61 | modifier: Modifier = Modifier, 62 | ) { 63 | val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } 64 | 65 | GoogleMap( 66 | modifier = modifier, 67 | cameraPositionState = cameraPositionState, 68 | ) { 69 | DraggableMarker( 70 | onDragStart = { Log.i(TAG, "onDragStart") }, 71 | onDragEnd = { Log.i(TAG, "onDragEnd") }, 72 | onDrag = { position -> Log.i(TAG, "onDrag: $position") } 73 | ) 74 | } 75 | } 76 | 77 | /** 78 | * A draggable GoogleMap Marker. 79 | * 80 | * @param onDragStart called when marker dragging starts 81 | * @param onDrag called with an update for the marker's current position during dragging 82 | * @param onDragEnd called when marker dragging ends 83 | */ 84 | @Composable 85 | private fun DraggableMarker( 86 | onDragStart: () -> Unit = {}, 87 | onDrag: (LatLng) -> Unit = {}, 88 | onDragEnd: () -> Unit = {} 89 | ) { 90 | val markerState = rememberMarkerState(position = singapore) 91 | 92 | Marker( 93 | state = markerState, 94 | draggable = true 95 | ) 96 | 97 | LaunchedEffect(Unit) { 98 | var inDrag = false 99 | var priorPosition: LatLng? = singapore 100 | 101 | snapshotFlow { markerState.isDragging to markerState.position } 102 | .dropWhile { (isDragging, position) -> 103 | !isDragging && position == priorPosition // ignore initial value 104 | } 105 | .collect { (isDragging, position) -> 106 | // Do not even bother to check isDragging state here: 107 | // it is possible to miss a sequence of states 108 | // where isDragging == true, then isDragging == false; 109 | // in this case we would only see a change in position. 110 | // (Hypothetically we could even miss a change in position 111 | // if the Marker ended up in its original position at the 112 | // end of the drag. But then nothing changed at all, 113 | // so we should be ok to ignore this case altogether.) 114 | if (!inDrag) { 115 | inDrag = true 116 | onDragStart() 117 | } 118 | 119 | if (position != priorPosition) { 120 | onDrag(position) 121 | priorPosition = position 122 | } 123 | 124 | if (!isDragging) { 125 | inDrag = false 126 | onDragEnd() 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose.markerexamples.markerscollection 16 | 17 | import android.os.Bundle 18 | import androidx.activity.ComponentActivity 19 | import androidx.activity.compose.setContent 20 | import androidx.activity.enableEdgeToEdge 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.layout.systemBarsPadding 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.Immutable 25 | import androidx.compose.runtime.key 26 | import androidx.compose.runtime.mutableStateMapOf 27 | import androidx.compose.ui.Modifier 28 | import com.google.android.gms.maps.model.LatLng 29 | import com.google.maps.android.compose.GoogleMap 30 | import com.google.maps.android.compose.MarkerState 31 | import com.google.maps.android.compose.defaultCameraPosition 32 | import com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel.Marker 33 | import com.google.maps.android.compose.rememberCameraPositionState 34 | import com.google.maps.android.compose.theme.MapsComposeSampleTheme 35 | 36 | /** 37 | * Simplistic app data model intended for persistent storage. 38 | * 39 | * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data. 40 | */ 41 | private class DataModel { 42 | /** 43 | * Location data. 44 | */ 45 | val locationDataMap = mutableStateMapOf() 46 | } 47 | 48 | /** 49 | * Data type representing a location. 50 | * 51 | * This only stores location position, for demonstration purposes, 52 | * but could hold other data related to the location. 53 | */ 54 | @Immutable 55 | private data class LocationData(val position: LatLng) 56 | 57 | /** 58 | * Unique, stable key for location 59 | */ 60 | private class LocationKey 61 | 62 | private typealias KeyedLocationData = Pair 63 | 64 | /** 65 | * Demonstrates how to sync a data model with a changing collection of 66 | * location markers using keys. 67 | * 68 | * The user can add a location marker to the model by clicking the map and delete a location from 69 | * the model by clicking a marker. 70 | * 71 | * This example reuses the simple non-draggable Marker approach from the 72 | * `UpdatingNoDragMarkerWithDataModelActivity` example, which encapsulates 73 | * [MarkerState] to provide a cleaner API surface. 74 | */ 75 | class MarkersCollectionActivity : ComponentActivity() { 76 | private val dataModel = DataModel() 77 | 78 | override fun onCreate(savedInstanceState: Bundle?) { 79 | super.onCreate(savedInstanceState) 80 | enableEdgeToEdge() 81 | setContent { 82 | MapsComposeSampleTheme { 83 | Screen( 84 | dataModel = dataModel, 85 | modifier = Modifier.fillMaxSize() 86 | .systemBarsPadding(), 87 | ) 88 | } 89 | } 90 | } 91 | } 92 | 93 | @Composable 94 | private fun Screen( 95 | dataModel: DataModel, 96 | modifier: Modifier = Modifier 97 | ) = GoogleMapWithLocations( 98 | modifier = modifier, 99 | keyedLocationData = dataModel.locationDataMap.toList(), 100 | onAddLocation = { locationData -> 101 | dataModel.locationDataMap += LocationKey() to locationData 102 | }, 103 | onDeleteLocation = { key -> 104 | dataModel.locationDataMap -= key 105 | } 106 | ) 107 | 108 | /** 109 | * A GoogleMap with locations represented by markers 110 | * 111 | * @param keyedLocationData model data for location markers with unique keys. 112 | * Uses a [Collection] type to keep it independent of our data model. 113 | * @param onAddLocation location addition events for updating data model 114 | * @param onDeleteLocation location deletion events for updating data model 115 | */ 116 | @Composable 117 | private fun GoogleMapWithLocations( 118 | keyedLocationData: Collection, 119 | modifier: Modifier = Modifier, 120 | onAddLocation: (LocationData) -> Unit, 121 | onDeleteLocation: (LocationKey) -> Unit 122 | ) { 123 | val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } 124 | 125 | GoogleMap( 126 | modifier = modifier, 127 | cameraPositionState = cameraPositionState, 128 | onMapClick = { position -> onAddLocation(LocationData(position)) } 129 | ) { 130 | Locations( 131 | keyedLocationData = keyedLocationData, 132 | onLocationClick = onDeleteLocation 133 | ) 134 | } 135 | } 136 | 137 | /** 138 | * Renders locations on a GoogleMap 139 | * 140 | * @param keyedLocationData model data for location markers with unique keys. 141 | * @param onLocationClick location click events 142 | */ 143 | @Composable 144 | private fun Locations( 145 | keyedLocationData: Collection, 146 | onLocationClick: (LocationKey) -> Unit 147 | ) = keyedLocationData.forEach { (key, locationData) -> 148 | key(key) { 149 | Marker( 150 | position = locationData.position, 151 | onClick = { 152 | onLocationClick(key) 153 | true // consume click event to prevent camera move to marker 154 | } 155 | ) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose.markerexamples.syncingdraggablemarkerwithdatamodel 16 | 17 | import android.os.Bundle 18 | import androidx.activity.ComponentActivity 19 | import androidx.activity.compose.setContent 20 | import androidx.activity.enableEdgeToEdge 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.layout.systemBarsPadding 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.Immutable 25 | import androidx.compose.runtime.LaunchedEffect 26 | import androidx.compose.runtime.getValue 27 | import androidx.compose.runtime.mutableStateOf 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.runtime.setValue 30 | import androidx.compose.runtime.snapshotFlow 31 | import androidx.compose.ui.Modifier 32 | import com.google.android.gms.maps.model.LatLng 33 | import com.google.maps.android.compose.GoogleMap 34 | import com.google.maps.android.compose.Marker 35 | import com.google.maps.android.compose.MarkerState 36 | import com.google.maps.android.compose.defaultCameraPosition 37 | import com.google.maps.android.compose.rememberCameraPositionState 38 | import com.google.maps.android.compose.singapore 39 | import com.google.maps.android.compose.theme.MapsComposeSampleTheme 40 | 41 | /** 42 | * Simplistic app data model intended for persistent storage. 43 | * 44 | * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data. 45 | */ 46 | private class DataModel { 47 | /** 48 | * Location data 49 | */ 50 | var locationData by mutableStateOf(LocationData(singapore)) 51 | } 52 | 53 | /** 54 | * Data type representing a location. 55 | * 56 | * This only stores location position, for demonstration purposes, 57 | * but could hold other data related to the location. 58 | */ 59 | @Immutable 60 | private data class LocationData(val position: LatLng) 61 | 62 | /** 63 | * Demonstrates how to avoid data races when keeping a data model in sync 64 | * with location derived from a draggable marker. The model is the initial source of truth for the 65 | * marker's position; markers are draggable, so MarkerState becomes the source of truth after 66 | * initialization. 67 | * 68 | * This addresses difficulties caused by having source of truth for position baked into 69 | * com.google.android.gms.maps.model.Marker, and consequently MarkerState. 70 | */ 71 | class SyncingDraggableMarkerWithDataModelActivity : ComponentActivity() { 72 | private val dataModel = DataModel() 73 | 74 | override fun onCreate(savedInstanceState: Bundle?) { 75 | super.onCreate(savedInstanceState) 76 | enableEdgeToEdge() 77 | setContent { 78 | MapsComposeSampleTheme { 79 | Screen( 80 | dataModel = dataModel, 81 | modifier = Modifier.fillMaxSize() 82 | .systemBarsPadding(), 83 | ) 84 | } 85 | } 86 | } 87 | } 88 | 89 | @Composable 90 | private fun Screen( 91 | dataModel: DataModel, 92 | modifier: Modifier = Modifier 93 | ) { 94 | GoogleMapWithLocation( 95 | modifier = modifier, 96 | locationData = dataModel.locationData, 97 | onUpdateLocation = { locationData -> 98 | dataModel.locationData = locationData 99 | } 100 | ) 101 | } 102 | 103 | /** 104 | * A GoogleMap with a location represented by a marker 105 | * 106 | * @param locationData model data for location marker. The UI becomes the source of truth for 107 | * marker position after initial composition; the model's position is ignored on recomposition. 108 | * @param onUpdateLocation location update events for updating data model 109 | */ 110 | @Composable 111 | private fun GoogleMapWithLocation( 112 | locationData: LocationData, 113 | modifier: Modifier = Modifier, 114 | onUpdateLocation: (LocationData) -> Unit 115 | ) { 116 | val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } 117 | 118 | GoogleMap( 119 | modifier = modifier, 120 | cameraPositionState = cameraPositionState, 121 | ) { 122 | LocationMarker( 123 | locationData = locationData, 124 | onLocationUpdate = onUpdateLocation 125 | ) 126 | } 127 | } 128 | 129 | /** 130 | * A draggable GoogleMap Marker representing a location on the map. 131 | * 132 | * @param locationData model data for location marker. The UI becomes the source of truth for 133 | * marker position after initial composition; the model's position is ignored on recomposition. 134 | * @param onLocationUpdate marker update events with updated [LocationData] 135 | */ 136 | @Composable 137 | private fun LocationMarker( 138 | locationData: LocationData, 139 | onLocationUpdate: (LocationData) -> Unit 140 | ) { 141 | // This sets the MarkerData from our model once (model is initial source of truth) 142 | // and never updates it from the model afterwards, 143 | // because MarkerState/GoogleMap is the source of truth after initialization 144 | // and we want to avoid multiple competing sources of truth 145 | // to prevent potential data races. 146 | // This achieves a clean separation of sources of truth at the cost of 147 | // no longer having state flow down. 148 | // It is the price we pay for having source of truth baked into 149 | // com.google.android.gms.maps.model.Marker, and consequently MarkerState. 150 | // 151 | // Do not use rememberMarkerState() here, because it uses rememberSaveable(); 152 | // we want to save the position to persistent storage as part of our data model 153 | // instead - rememberSaveable() would add a conflicting source of truth. 154 | val markerState = remember { MarkerState(locationData.position) } 155 | 156 | Marker( 157 | state = markerState, 158 | draggable = true 159 | ) 160 | 161 | LaunchedEffect(Unit) { 162 | snapshotFlow { markerState.position } 163 | .collect { position -> 164 | // build LocationData update from marker update 165 | val update = LocationData(position = position) 166 | 167 | // send update event 168 | onLocationUpdate(update) 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel 16 | 17 | import android.os.Bundle 18 | import androidx.activity.ComponentActivity 19 | import androidx.activity.compose.setContent 20 | import androidx.activity.enableEdgeToEdge 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.layout.systemBarsPadding 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.Immutable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.setValue 28 | import androidx.compose.ui.Modifier 29 | import androidx.lifecycle.lifecycleScope 30 | import com.google.android.gms.maps.model.LatLng 31 | import com.google.maps.android.compose.GoogleMap 32 | import com.google.maps.android.compose.Marker 33 | import com.google.maps.android.compose.defaultCameraPosition 34 | import com.google.maps.android.compose.rememberCameraPositionState 35 | import com.google.maps.android.compose.rememberUpdatedMarkerState 36 | import com.google.maps.android.compose.singapore 37 | import com.google.maps.android.compose.singapore2 38 | import com.google.maps.android.compose.singapore3 39 | import com.google.maps.android.compose.theme.MapsComposeSampleTheme 40 | import kotlinx.coroutines.delay 41 | import kotlinx.coroutines.launch 42 | import kotlin.random.Random 43 | 44 | /** 45 | * Simplistic app data model intended for persistent storage. 46 | * 47 | * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data. 48 | */ 49 | private class DataModel { 50 | /** 51 | * Location data 52 | */ 53 | var locationData by mutableStateOf(LocationData(singapore)) 54 | } 55 | 56 | /** 57 | * Data type representing a location. 58 | * 59 | * This only stores location position, for demonstration purposes, 60 | * but could hold other data related to the location. 61 | */ 62 | @Immutable 63 | private data class LocationData(val position: LatLng) 64 | 65 | /** 66 | * Demonstrates how to easily initialize and update position for a non-draggable 67 | * Marker from a data model. 68 | */ 69 | class UpdatingNoDragMarkerWithDataModelActivity : ComponentActivity() { 70 | private val dataModel = DataModel() 71 | 72 | override fun onCreate(savedInstanceState: Bundle?) { 73 | super.onCreate(savedInstanceState) 74 | enableEdgeToEdge() 75 | lifecycleScope.launch { 76 | // Simulate remote updates to data model 77 | while (true) { 78 | delay(3_000) 79 | 80 | val newPosition = when (Random.nextInt(3)) { 81 | 0 -> singapore 82 | 1 -> singapore2 83 | 2 -> singapore3 84 | else -> singapore 85 | } 86 | 87 | dataModel.locationData = LocationData(newPosition) 88 | } 89 | } 90 | 91 | setContent { 92 | MapsComposeSampleTheme { 93 | GoogleMapWithSimpleMarker( 94 | locationData = dataModel.locationData, 95 | modifier = Modifier.fillMaxSize() 96 | .systemBarsPadding(), 97 | ) 98 | } 99 | } 100 | } 101 | } 102 | 103 | @Composable 104 | private fun GoogleMapWithSimpleMarker( 105 | locationData: LocationData, 106 | modifier: Modifier = Modifier, 107 | ) { 108 | val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } 109 | 110 | GoogleMap( 111 | modifier = modifier, 112 | cameraPositionState = cameraPositionState, 113 | ) { 114 | Marker(position = locationData.position) 115 | } 116 | } 117 | 118 | /** 119 | * Standard API pattern for a non-draggable Marker. 120 | * 121 | * The caller does not have to deal with MarkerState, 122 | * and can update Marker [position] via recomposition. 123 | */ 124 | @Composable 125 | fun Marker( 126 | position: LatLng, 127 | onClick: () -> Boolean = { false }, 128 | ) { 129 | val markerState = rememberUpdatedMarkerState(position = position) 130 | 131 | Marker( 132 | state = markerState, 133 | onClick = { onClick() } 134 | ) 135 | } -------------------------------------------------------------------------------- /maps-app/src/main/java/com/google/maps/android/compose/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.google.maps.android.compose.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | @Composable 10 | fun MapsComposeSampleTheme( 11 | darkTheme: Boolean = isSystemInDarkTheme(), 12 | content: @Composable () -> Unit 13 | ) { 14 | MaterialTheme( 15 | colors = if (darkTheme) darkColors() else lightColors(), 16 | content = content 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /maps-app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 25 | 31 | 34 | 37 | 38 | 39 | 40 | 46 | -------------------------------------------------------------------------------- /maps-app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 176 | 181 | 186 | 187 | -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 23 | 25 | -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/android-maps-compose/807c957d8509a7258dc1f070bd979a6785e21de3/maps-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /maps-app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | android-maps-compose 19 | "Maps Compose Demos \uD83D\uDDFA" 20 | Basic Map 21 | Advanced Markers 22 | Map In Column 23 | Marker Clustering 24 | Marker Drag Events 25 | Markers Collection 26 | Location Tracking 27 | Scale Bar 28 | Syncing Draggable Marker With Model 29 | Updating Non-Draggable Marker With Model 30 | Polygon around draggable markers 31 | Recomposition Map 32 | Street View 33 | Custom Location Button 34 | Accessibility 35 | Maps in LazyColumn 36 | -------------------------------------------------------------------------------- /maps-app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 |