├── .github └── workflows │ ├── publish.yml │ └── snapshot.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASING.md ├── build.gradle ├── common.gradle ├── googlemaps ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── nz │ └── co │ └── trademe │ └── mapme │ └── googlemaps │ ├── Extensions.kt │ ├── GoogleMapAnnotationFactory.kt │ ├── GoogleMapMarkerAnnotation.kt │ └── GoogleMapMeAdapter.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img └── feature.png ├── lint.xml ├── mapbox ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── nz │ └── co │ └── trademe │ └── mapme │ └── mapbox │ ├── Extensions.kt │ ├── MapboxAnnotationFactory.kt │ ├── MapboxMapMeAdapter.kt │ └── MapboxMarkerAnnotation.kt ├── mapme ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── nz │ │ └── co │ │ └── trademe │ │ └── mapme │ │ ├── LatLng.kt │ │ ├── MapAdapterHelper.kt │ │ ├── MapMeAdapter.kt │ │ ├── OpReorderer.java │ │ ├── UpdateOp.java │ │ └── annotations │ │ ├── AnnotationFactory.kt │ │ ├── MapAnnotation.kt │ │ ├── MarkerAnnotation.kt │ │ ├── OnInfoWindowClickListener.java │ │ ├── OnMapAnnotationClickListener.java │ │ └── Placeholder.kt │ └── test │ └── java │ └── nz │ └── co │ └── trademe │ └── mapme │ ├── AdapterHelperTest.kt │ ├── BaseAdapterTest.kt │ ├── move │ └── AdapterTestAdvanced.kt │ ├── simple │ └── AdapterTestSimple.kt │ └── util │ ├── ItemsHelper.kt │ ├── TestAdapter.kt │ ├── TestAnnotation.kt │ ├── TestAnnotationFactory.kt │ └── TestItem.kt ├── publish-root.gradle ├── publishing.gradle ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── nz │ │ └── co │ │ └── trademe │ │ └── mapme │ │ └── sample │ │ ├── Constants.java │ │ ├── MarkerBottomSheet.java │ │ ├── MarkerData.java │ │ ├── SampleApplication.java │ │ ├── SampleMapMeAdapter.java │ │ ├── Util.java │ │ └── activities │ │ ├── BaseActivity.java │ │ ├── ChoiceActivity.java │ │ ├── GoogleMapsActivity.java │ │ ├── MapActivity.java │ │ └── MapBoxActivity.java │ └── res │ ├── drawable-hdpi │ ├── marker_blue.png │ ├── marker_green.png │ └── marker_red.png │ ├── drawable-mdpi │ ├── marker_blue.png │ ├── marker_green.png │ └── marker_red.png │ ├── drawable-xhdpi │ ├── marker_blue.png │ ├── marker_green.png │ └── marker_red.png │ ├── drawable-xxhdpi │ ├── marker_blue.png │ ├── marker_green.png │ └── marker_red.png │ ├── drawable-xxxhdpi │ ├── marker_blue.png │ ├── marker_green.png │ └── marker_red.png │ ├── layout │ ├── choice_activity.xml │ ├── googlemaps_activity.xml │ ├── mapbox_activity.xml │ └── marker_bottom_sheet.xml │ ├── menu │ └── menu.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish: 10 | name: Release build and publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v2 15 | - name: Set up JDK 11 16 | uses: actions/setup-java@v2 17 | with: 18 | distribution: adopt 19 | java-version: 11 20 | 21 | # Builds the release artifacts of the library 22 | - name: Release build 23 | run: ./gradlew googlemaps:assembleRelease -x :mapbox:assembleRelease -x :mapme:assembleRelease 24 | 25 | # Generates other artifacts 26 | - name: Source jar and dokka 27 | run: ./gradlew androidSourcesJar javadocJar 28 | 29 | # Runs upload, and then closes & releases the repository 30 | - name: Publish to MavenCentral 31 | run: ./gradlew publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseSonatypeStagingRepository 32 | env: 33 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 34 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 35 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 36 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 37 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 38 | SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} 39 | SNAPSHOT: false -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish Snapshot builds 2 | 3 | 4 | on: 5 | push: 6 | branches: 7 | - 'release/**' 8 | jobs: 9 | publish: 10 | name: Snapshot build and publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v2 15 | - name: Set up JDK 11 16 | uses: actions/setup-java@v2 17 | with: 18 | distribution: adopt 19 | java-version: 11 20 | - name: Release build 21 | 22 | run: ./gradlew googlemaps:assembleRelease -x :mapbox:assembleRelease -x :mapme:assembleRelease 23 | - name: Source jar and dokka 24 | run: ./gradlew androidSourcesJar javadocJar 25 | - name: Publish to MavenCentral 26 | run: ./gradlew publishReleasePublicationToSonatypeRepository 27 | 28 | 29 | 30 | env: 31 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 32 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 33 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 34 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 35 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 36 | SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} 37 | SNAPSHOT: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /.idea 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | android: 4 | components: 5 | - tools 6 | - platform-tools 7 | 8 | before_install: 9 | - mkdir "$ANDROID_HOME/licenses" || true 10 | - echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" 11 | - echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license" 12 | - echo "84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" 13 | 14 | jdk: 15 | - oraclejdk8 16 | 17 | sudo: false 18 | 19 | cache: 20 | directories: 21 | - $HOME/.m2 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | Version 1.2.1 *(28-03-2022)* 4 | ---------------------------- 5 | 6 | * Migration to maven central 7 | 8 | Version 1.2.0 *(25-10-2018)* 9 | ---------------------------- 10 | 11 | * Updates to Kotlin JDK dependency variants 12 | * Updates support library to AndroidX variants 13 | 14 | Version 1.1.0 *(06-07-2018)* 15 | ---------------------------- 16 | 17 | * Adds support for custom anchoring of Markers 18 | 19 | Version 1.0.6 *(01-05-2018)* 20 | ---------------------------- 21 | 22 | * Removes Bintray gradle plugin in favour of `maven-publishing` 23 | 24 | Version 1.0.5 *(13-04-2018)* 25 | ---------------------------- 26 | 27 | * Updates to AGP and dependencies 28 | * Fixes crash when notifyDataSetChanged called multiple times 29 | 30 | Version 1.0.4 *(21-09-2017)* 31 | ---------------------------- 32 | 33 | * Updates Google Play Services to 11.4.0 34 | * Updates Android Support Library to 26.1.0 35 | 36 | Version 1.0.3 *(18-09-2017)* 37 | ---------------------------- 38 | 39 | * Updates Mapbox SDK to 5.1.3 40 | 41 | Version 1.0.2 *(08-09-2017)* 42 | ---------------------------- 43 | 44 | * OnAnnotationClickListener and OnInfoWindowClickListener now support SAM conversion 45 | 46 | Version 1.0.1 *(07-09-2017)* 47 | ---------------------------- 48 | 49 | * Simplifies the API (removing unnecessary type information) 50 | * Adds support for map info windows 51 | * Fixes info window titles not being set 52 | 53 | Version 1.0.0 *(30-08-2017)* 54 | ---------------------------- 55 | 56 | * Initial release -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @athornz -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions to this repository. 4 | 5 | To avoid wasting anyone's time, ensure you *first discuss the change you wish to make by opening an issue*. 6 | 7 | All contributions must follow the conventions, code style and formatting of this repository. 8 | 9 | Other things to consider: 10 | * all tests should pass 11 | * new features should be accompanied by tests 12 | * breaking changes should be avoided 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Trade Me 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MapMe 2 | 3 | ![MapMe](./img/feature.png) 4 | 5 | [ ![Download](https://api.bintray.com/packages/trademe/MapMe/mapme/images/download.svg) ](https://bintray.com/trademe/MapMe/mapme/_latestVersion) 6 | [![Build Status](https://travis-ci.org/TradeMe/MapMe.svg?branch=master)](https://travis-ci.org/TradeMe/MapMe) 7 | 8 | MapMe is an Android library for working with Maps. MapMe brings the adapter pattern to Maps, simplifying the management of markers and annotations. 9 | 10 | MapMe supports both [Google Maps](https://developers.google.com/maps/documentation/android-api/) and [Mapbox](https://www.mapbox.com/android-sdk/) 11 | 12 | 13 | Download 14 | ----- 15 | 16 | ```groovy 17 | //base dependency 18 | compile 'nz.co.trademe.mapme:mapme:1.2.1' 19 | 20 | //for Google Maps support 21 | compile 'nz.co.trademe.mapme:googlemaps:1.2.1' 22 | 23 | //for Mapbox support 24 | compile 'nz.co.trademe.mapme:mapbox:1.2.1' 25 | 26 | ``` 27 | 28 | Usage 29 | ----- 30 | A simple MapsAdapter might look like this: 31 | 32 | ```kotlin 33 | class MapsAdapter(context: Context, private val markers: List) : GoogleMapMeAdapter(context) { 34 | 35 | fun onCreateAnnotation(factory: AnnotationFactory, position: Int, annotationType: Int): MapAnnotation { 36 | val item = this.markers[position] 37 | return factory.createMarker(item.getLatLng(), null, item.getTitle()) 38 | } 39 | 40 | fun onBindAnnotation(annotation: MapAnnotation, position: Int, payload: Any) { 41 | if (annotation is MarkerAnnotation) { 42 | val item = this.markers[position] 43 | annotation.setTitle(item.getTitle()) 44 | } 45 | } 46 | 47 | val itemCount: Int 48 | get() = markers.size() 49 | } 50 | 51 | ``` 52 | 53 | Using the adapter in your view: 54 | 55 | ```kotlin 56 | val adapter: MapMeAdapter = GoogleMapMeAdapter(context, items) 57 | adapter.setOnAnnotationClickListener(this) 58 | 59 | mapView.getMapAsync { googleMap -> 60 | //Attach the adapter to the map view once it's initialized 61 | adapter.attach(mapView, googleMap) 62 | } 63 | ``` 64 | 65 | Dispatch data updates to the adapter: 66 | 67 | ```kotlin 68 | // add new data and tell the adapter about it 69 | 70 | items.addAll(myData) 71 | adapter.notifyDataSetChanged() 72 | 73 | // or with DiffUtil 74 | 75 | val diff = DiffUtil.calculateDiff(myDiffCallback) 76 | diff.dispatchUpdatesTo(adapter) 77 | ``` 78 | 79 | 80 | Click listeners 81 | ----- 82 | MapMe takes the pain out of click listeners too. No more setting tags on markers and trying to match a tag to your data when the click event is received. 83 | 84 | MapMe has a `setOnAnnotationClickListener` method that will pass back a `MapAnnotation` containing the position of the item in the list of data: 85 | 86 | ```Kotlin 87 | mapsAdapter.setOnAnnotationClickListener(OnMapAnnotationClickListener { annotation -> 88 | //retrieve the data item based on the position 89 | val item = myData[annotation.position] 90 | 91 | //handle item click here 92 | 93 | true 94 | }) 95 | 96 | ``` 97 | 98 | **Info window** clicks are handled in the same way. 99 | 100 | Animations 101 | ----- 102 | 103 | While MapMe doesn't handle marker animations directly, it does provide a `onAnnotationAdded` method on the adapter that is called when a marker is added to the map. 104 | 105 | This is the ideal place to start an animation. 106 | 107 | 108 | For example, the following animates a markers alpha when it is added to the map: 109 | 110 | 111 | ``` 112 | override fun onAnnotationAdded(annotation: MapAnnotation) { 113 | if (annotation !is MarkerAnnotation) return 114 | 115 | ObjectAnimator.ofFloat(annotation, "alpha", 0f, 1f) 116 | .apply { 117 | duration = 150 118 | interpolator = DecelerateInterpolator() 119 | start() 120 | } 121 | } 122 | ``` 123 | 124 | 125 | Markers and Annotations 126 | ----- 127 | MapMe is based around the concept of Annotations. An annotation is anything displayed on the map. 128 | 129 | The only annotation currently supported is Markers. We hope to support many more in the future. 130 | 131 | *We'd love PR's adding support for more annotations!* 132 | 133 | 134 | ### Multiple annotation types 135 | More complex adapters can override `getItemAnnotationType` to work with multiple annotations. The annotation type is passed to `onCreateAnnotation` just like in a RecyclerView Adapter. 136 | 137 | 138 | ### AnnotationFactory 139 | MapMe differs from list adapters in that the creation of annotations must be left up to the map as they are not standard Android views. 140 | 141 | The MapAdapter **onCreateAnnotation** method provides an **AnnotationFactory** as a parameter that must be used to create and return Map Annotations. 142 | 143 | 144 | DiffUtil 145 | ----- 146 | As well as support for standard Adapter methods such as *notifyDataSetChanged*, and *notifyItemInserted*, MapMe supports (and recommends) DiffUtil. 147 | 148 | DiffUtil is where the true power of MapMe comes into play. Simple manipulate the data set, calculate the diff and dispatch it to MapMe. The map will instantly reflect the data. 149 | 150 | A DiffResult can be dispatched to the MapAdapter just as you would a RecyclerView: 151 | 152 | ```java 153 | DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MarkerDiffCallback(this.markers, newMarkers)); 154 | diffResult.dispatchUpdatesTo(mapAdapter); 155 | ``` 156 | 157 | 158 | Why the adapter pattern? 159 | ----- 160 | 161 | Working with a few map markers is simple, but working with hundreds can become a mess of spaghetti code. 162 | 163 | The adapter pattern provides a clear separation of data from the view, allowing the data to be manipulated freely without the concern of updating the view. 164 | 165 | We think this is a pattern fits perfectly with maps. 166 | 167 | 168 | ## Contributing 169 | 170 | We love contributions, but make sure to checkout `CONTRIBUTING.MD` first! 171 | 172 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ======== 3 | 4 | 1. Change the version in `common.gradle` to a non-SNAPSHOT version. 5 | 2. Update the `CHANGELOG.md` for the impending release. 6 | 3. Update the `README.md` with the new version. 7 | 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version) 8 | 5. `./gradlew clean build publishAarPublicationToBintrayRepository`. 9 | 6. Visit [Bintray](https://bintray.com) and publish the artifact. 10 | 7. `git tag -a X.Y.X -m "Version X.Y.Z"` (where X.Y.Z is the new version) 11 | 8. Update the version in `common.gradle` to the next SNAPSHOT version. 12 | 9. `git commit -am "Prepare next development version."` 13 | 10. `git push && git push --tags` 14 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'io.github.gradle-nexus.publish-plugin' 2 | apply plugin: 'org.jetbrains.dokka' 3 | 4 | buildscript { 5 | apply from: 'common.gradle' 6 | 7 | repositories { 8 | maven { url "https://plugins.gradle.org/m2/" } 9 | google() 10 | jcenter() 11 | mavenCentral() 12 | } 13 | dependencies { 14 | classpath "com.android.tools.build:gradle:$gradle_version" 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 | classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' 17 | classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.6.10' 18 | } 19 | } 20 | 21 | allprojects { 22 | repositories { 23 | google() 24 | jcenter() 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | apply from: "${rootDir}/publish-root.gradle" -------------------------------------------------------------------------------- /common.gradle: -------------------------------------------------------------------------------- 1 | ext.version = "1.2.1" 2 | ext.group = "nz.co.trademe.mapme" 3 | ext.repo = "mapme" 4 | ext.org = "trademe" 5 | ext.scm = 'https://github.com/TradeMe/MapMe.git' 6 | ext.url = 'https://github.com/TradeMe/MapMe' 7 | ext.description = 'MapMe is an Android library that brings the adapter pattern to Maps, simplifying the management of markers and annotations.' 8 | 9 | ext.connection = 'scm:git:github.com/TradeMe/MapMe.git' 10 | ext.developerConnection = 'scm:git:ssh://github.com/TradeMe/MapMe.git' 11 | 12 | ext.compileSdk = 30 13 | ext.minSdk = 19 14 | ext.buildTools = "29.0.3" 15 | ext.kotlin_version = '1.5.0' 16 | ext.dokka_version = '0.9.15' 17 | ext.gradle_version = '4.0.1' 18 | ext.mapbox_version = '6.4.0' 19 | ext.google_play_services_version = '15.0.1' 20 | ext.androidx_version = '1.0.0' 21 | ext.android_maven_version = '3.6.2' 22 | 23 | -------------------------------------------------------------------------------- /googlemaps/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /googlemaps/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | apply from: '../common.gradle' 3 | repositories { 4 | google() 5 | jcenter() 6 | mavenLocal() 7 | } 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | apply from: '../common.gradle' 14 | 15 | ext { 16 | PUBLISH_VERSION = rootVersionName 17 | ARTIFACT_ID = "googlemaps" 18 | } 19 | 20 | 21 | apply plugin: 'com.android.library' 22 | apply plugin: 'kotlin-android' 23 | apply from: '../publishing.gradle' 24 | 25 | android { 26 | compileSdkVersion compileSdk 27 | buildToolsVersion buildTools 28 | 29 | defaultConfig { 30 | minSdkVersion minSdk 31 | targetSdkVersion compileSdk 32 | 33 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 34 | 35 | } 36 | buildTypes { 37 | release { 38 | minifyEnabled false 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | } 41 | } 42 | 43 | lintOptions { 44 | lintConfig new File(rootProject.projectDir, "lint.xml") 45 | } 46 | } 47 | 48 | repositories { 49 | google() 50 | jcenter() 51 | } 52 | 53 | dependencies { 54 | compile "com.google.android.gms:play-services-maps:$google_play_services_version" 55 | compile "androidx.appcompat:appcompat:$androidx_version" 56 | 57 | compile project(':mapme') 58 | 59 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 60 | } 61 | 62 | 63 | -------------------------------------------------------------------------------- /googlemaps/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jburton/dev/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /googlemaps/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /googlemaps/src/main/java/nz/co/trademe/mapme/googlemaps/Extensions.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("GoogleMapUtils") 2 | package nz.co.trademe.mapme.googlemaps 3 | 4 | import android.graphics.Bitmap 5 | import com.google.android.gms.maps.model.BitmapDescriptor 6 | import com.google.android.gms.maps.model.BitmapDescriptorFactory 7 | import nz.co.trademe.mapme.LatLng 8 | 9 | 10 | fun LatLng.toGoogleMapsLatLng(): com.google.android.gms.maps.model.LatLng { 11 | return com.google.android.gms.maps.model.LatLng(this.latitude, this.longitude) 12 | } 13 | 14 | fun Bitmap.toBitmapDescriptor(): BitmapDescriptor { 15 | return BitmapDescriptorFactory.fromBitmap(this) 16 | } -------------------------------------------------------------------------------- /googlemaps/src/main/java/nz/co/trademe/mapme/googlemaps/GoogleMapAnnotationFactory.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.googlemaps 2 | 3 | import android.graphics.Bitmap 4 | import com.google.android.gms.maps.GoogleMap 5 | import nz.co.trademe.mapme.LatLng 6 | import nz.co.trademe.mapme.annotations.AnnotationFactory 7 | import nz.co.trademe.mapme.annotations.MarkerAnnotation 8 | 9 | class GoogleMapAnnotationFactory : AnnotationFactory { 10 | 11 | override fun createMarker(latLng: LatLng, icon: Bitmap?, title: String?): MarkerAnnotation { 12 | return GoogleMapMarkerAnnotation(latLng, title, icon) 13 | } 14 | 15 | override fun clear(map: GoogleMap) { 16 | map.clear() 17 | } 18 | 19 | override fun setOnMarkerClickListener(map: GoogleMap, onClick: (marker: Any) -> Boolean) { 20 | map.setOnMarkerClickListener { marker -> onClick(marker) } 21 | } 22 | 23 | override fun setOnInfoWindowClickListener(map: GoogleMap, onClick: (marker: Any) -> Boolean) { 24 | map.setOnInfoWindowClickListener { marker -> onClick(marker) } 25 | } 26 | } -------------------------------------------------------------------------------- /googlemaps/src/main/java/nz/co/trademe/mapme/googlemaps/GoogleMapMarkerAnnotation.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.googlemaps 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import com.google.android.gms.maps.GoogleMap 6 | import com.google.android.gms.maps.model.Marker 7 | import com.google.android.gms.maps.model.MarkerOptions 8 | import nz.co.trademe.mapme.LatLng 9 | import nz.co.trademe.mapme.annotations.MarkerAnnotation 10 | 11 | class GoogleMapMarkerAnnotation(latLng: LatLng, 12 | title: String?, 13 | icon: Bitmap? = null) : MarkerAnnotation(latLng, title, icon) { 14 | 15 | override fun onUpdateIcon(icon: Bitmap?) { 16 | nativeMarker?.setIcon(icon?.toBitmapDescriptor()) 17 | } 18 | 19 | override fun onUpdateTitle(title: String?) { 20 | nativeMarker?.title = title 21 | } 22 | 23 | override fun onUpdatePosition(position: LatLng) { 24 | nativeMarker?.position = position.toGoogleMapsLatLng() 25 | } 26 | 27 | override fun onUpdateZIndex(index: Float) { 28 | nativeMarker?.zIndex = index 29 | } 30 | 31 | override fun onUpdateAlpha(alpha: Float) { 32 | nativeMarker?.alpha = alpha 33 | } 34 | 35 | override fun onUpdateAnchor(anchorUV: Pair) { 36 | nativeMarker?.setAnchor(anchorUV.first, anchorUV.second) 37 | } 38 | 39 | private var nativeMarker: Marker? = null 40 | 41 | override fun annotatesObject(objec: Any): Boolean { 42 | return nativeMarker?.equals(objec) ?: false 43 | } 44 | 45 | override fun removeFromMap(map: Any, context: Context) { 46 | nativeMarker?.remove() 47 | nativeMarker = null 48 | } 49 | 50 | override fun addToMap(map: Any, context: Context) { 51 | val googleMap = map as GoogleMap 52 | 53 | val options = MarkerOptions() 54 | .position(latLng.toGoogleMapsLatLng()) 55 | .icon(icon?.toBitmapDescriptor()) 56 | .title(title) 57 | .alpha(alpha) 58 | .zIndex(zIndex) 59 | 60 | nativeMarker = googleMap.addMarker(options) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /googlemaps/src/main/java/nz/co/trademe/mapme/googlemaps/GoogleMapMeAdapter.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.googlemaps 2 | 3 | import android.content.Context 4 | import com.google.android.gms.maps.GoogleMap 5 | import nz.co.trademe.mapme.MapMeAdapter 6 | import nz.co.trademe.mapme.annotations.MapAnnotation 7 | import nz.co.trademe.mapme.annotations.OnInfoWindowClickListener 8 | 9 | abstract class GoogleMapMeAdapter(context: Context) : MapMeAdapter(context, GoogleMapAnnotationFactory()){ 10 | } 11 | 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536m 2 | #Disables an error with Gradle that is fixed in Gradle 4.4 but an error is still thrown with AGP 3.0.1 3 | #error will be removed in 3.1.0 4 | #See https://d.android.com/r/tools/buildscript-classpath-check.html 5 | android.enableBuildScriptClasspathCheck=false 6 | 7 | # Bintray credentials - When publishing, ensure changes to this file are not added to source control 8 | BINTRAY_USERNAME=INSERT_USERNAME_HERE 9 | BINTRAY_API_KEY=INSERT_KEY_HERE 10 | android.useAndroidX=true 11 | android.enableJetifier=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Mar 10 13:26:55 NZDT 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /img/feature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/img/feature.png -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /mapbox/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /mapbox/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | apply from: '../common.gradle' 3 | repositories { 4 | google() 5 | jcenter() 6 | mavenLocal() 7 | } 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | apply from: '../common.gradle' 14 | 15 | ext { 16 | PUBLISH_VERSION = rootVersionName 17 | ARTIFACT_ID = "mapbox" 18 | } 19 | 20 | apply plugin: 'com.android.library' 21 | apply plugin: 'kotlin-android' 22 | apply from: '../publishing.gradle' 23 | 24 | android { 25 | compileSdkVersion compileSdk 26 | buildToolsVersion buildTools 27 | 28 | defaultConfig { 29 | minSdkVersion minSdk 30 | targetSdkVersion compileSdk 31 | 32 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 33 | 34 | } 35 | buildTypes { 36 | release { 37 | minifyEnabled false 38 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 39 | } 40 | } 41 | 42 | lintOptions { 43 | lintConfig new File(rootProject.projectDir, "lint.xml") 44 | } 45 | 46 | compileOptions { 47 | sourceCompatibility 1.8 48 | targetCompatibility 1.8 49 | } 50 | } 51 | 52 | repositories { 53 | google() 54 | jcenter() 55 | } 56 | 57 | dependencies { 58 | compile("com.mapbox.mapboxsdk:mapbox-android-sdk:$mapbox_version@aar") { 59 | transitive = true 60 | } 61 | 62 | compile project(':mapme') 63 | 64 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 65 | } -------------------------------------------------------------------------------- /mapbox/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jburton/dev/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /mapbox/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /mapbox/src/main/java/nz/co/trademe/mapme/mapbox/Extensions.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("MapBoxUtils") 2 | package nz.co.trademe.mapme.mapbox 3 | 4 | import android.content.Context 5 | import android.graphics.Bitmap 6 | import com.mapbox.mapboxsdk.annotations.Icon 7 | import com.mapbox.mapboxsdk.annotations.IconFactory 8 | import nz.co.trademe.mapme.LatLng 9 | 10 | fun LatLng.toMapBoxLatLng(): com.mapbox.mapboxsdk.geometry.LatLng { 11 | return com.mapbox.mapboxsdk.geometry.LatLng(this.latitude, this.longitude) 12 | } 13 | 14 | fun Bitmap.toMapboxIcon(context: Context): Icon { 15 | return IconFactory.getInstance(context).fromBitmap(this) 16 | } 17 | -------------------------------------------------------------------------------- /mapbox/src/main/java/nz/co/trademe/mapme/mapbox/MapboxAnnotationFactory.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.mapbox 2 | 3 | import android.graphics.Bitmap 4 | import com.mapbox.mapboxsdk.maps.MapboxMap 5 | import nz.co.trademe.mapme.LatLng 6 | import nz.co.trademe.mapme.annotations.AnnotationFactory 7 | import nz.co.trademe.mapme.annotations.MarkerAnnotation 8 | 9 | class MapboxAnnotationFactory : AnnotationFactory { 10 | 11 | override fun createMarker(latLng: LatLng, icon: Bitmap?, title: String?): MarkerAnnotation { 12 | return MapboxMarkerAnnotation(latLng, title, icon) 13 | } 14 | 15 | override fun clear(map: MapboxMap) { 16 | map.clear() 17 | } 18 | 19 | override fun setOnMarkerClickListener(map: MapboxMap, onClick: (marker: Any) -> Boolean) { 20 | map.setOnMarkerClickListener { marker -> onClick(marker) } 21 | } 22 | 23 | override fun setOnInfoWindowClickListener(map: MapboxMap, onClick: (marker: Any) -> Boolean) { 24 | map.setOnInfoWindowClickListener { marker -> onClick(marker) } 25 | } 26 | } -------------------------------------------------------------------------------- /mapbox/src/main/java/nz/co/trademe/mapme/mapbox/MapboxMapMeAdapter.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.mapbox 2 | 3 | import android.content.Context 4 | import com.mapbox.mapboxsdk.maps.MapboxMap 5 | import nz.co.trademe.mapme.MapMeAdapter 6 | 7 | abstract class MapboxMapMeAdapter(context: Context) : MapMeAdapter(context, MapboxAnnotationFactory()) -------------------------------------------------------------------------------- /mapbox/src/main/java/nz/co/trademe/mapme/mapbox/MapboxMarkerAnnotation.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.mapbox 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.util.Log 6 | import com.mapbox.mapboxsdk.annotations.IconFactory 7 | import com.mapbox.mapboxsdk.annotations.Marker 8 | import com.mapbox.mapboxsdk.annotations.MarkerOptions 9 | import com.mapbox.mapboxsdk.maps.MapboxMap 10 | import nz.co.trademe.mapme.LatLng 11 | import nz.co.trademe.mapme.annotations.MarkerAnnotation 12 | 13 | class MapboxMarkerAnnotation(latLng: LatLng, 14 | title: String?, 15 | icon: Bitmap? = null) : MarkerAnnotation(latLng, title, icon) { 16 | 17 | override fun onUpdateIcon(icon: Bitmap?) { 18 | nativeMarker?.let { 19 | if (icon != null) { 20 | nativeMarker?.icon = (IconFactory.recreate(nativeMarker!!.icon.id, icon)) 21 | } else { 22 | nativeMarker?.icon = null 23 | } 24 | } 25 | } 26 | 27 | override fun onUpdateTitle(title: String?) { 28 | nativeMarker?.title = title 29 | } 30 | 31 | override fun onUpdatePosition(position: LatLng) { 32 | nativeMarker?.position = position.toMapBoxLatLng() 33 | } 34 | 35 | override fun onUpdateZIndex(index: Float) { 36 | Log.w(MapboxMarkerAnnotation::class.simpleName, "zIndex not supported on MapboxMarkerAnnotations") 37 | } 38 | 39 | override fun onUpdateAlpha(alpha: Float) { 40 | Log.w(MapboxMarkerAnnotation::class.simpleName, "alpha not supported on MapboxMarkerAnnotations") 41 | } 42 | 43 | override fun onUpdateAnchor(anchorUV: Pair) { 44 | Log.w(MapboxMarkerAnnotation::class.simpleName, "anchor not supported on MapboxMarkerAnnotations") 45 | } 46 | 47 | private var nativeMarker: Marker? = null 48 | 49 | override fun annotatesObject(objec: Any): Boolean { 50 | return nativeMarker?.equals(objec) ?: false 51 | } 52 | 53 | override fun removeFromMap(map: Any, context: Context) { 54 | nativeMarker?.remove() 55 | nativeMarker = null 56 | } 57 | 58 | override fun addToMap(map: Any, context: Context) { 59 | val mapboxMap = map as MapboxMap 60 | val markerOptions = MarkerOptions() 61 | .icon(icon?.toMapboxIcon(context)) 62 | .title(title) 63 | .position(latLng.toMapBoxLatLng()) 64 | nativeMarker = mapboxMap.addMarker(markerOptions) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /mapme/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /mapme/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | apply from: '../common.gradle' 3 | repositories { 4 | google() 5 | jcenter() 6 | mavenLocal() 7 | } 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | 14 | 15 | apply from: '../common.gradle' 16 | 17 | ext { 18 | PUBLISH_VERSION = rootVersionName 19 | ARTIFACT_ID = "mapme" 20 | } 21 | 22 | 23 | apply plugin: 'com.android.library' 24 | apply plugin: 'kotlin-android' 25 | apply from: '../publishing.gradle' 26 | 27 | 28 | android { 29 | compileSdkVersion compileSdk 30 | buildToolsVersion buildTools 31 | 32 | defaultConfig { 33 | minSdkVersion minSdk 34 | targetSdkVersion compileSdk 35 | 36 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 37 | } 38 | 39 | buildTypes { 40 | release { 41 | minifyEnabled false 42 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 43 | } 44 | } 45 | 46 | lintOptions { 47 | lintConfig new File(rootProject.projectDir, "lint.xml") 48 | } 49 | 50 | testOptions { 51 | unitTests.returnDefaultValues = true 52 | } 53 | 54 | compileOptions { 55 | sourceCompatibility 1.8 56 | targetCompatibility 1.8 57 | } 58 | } 59 | 60 | repositories { 61 | google() 62 | jcenter() 63 | } 64 | 65 | dependencies { 66 | testCompile 'junit:junit:4.12' 67 | testCompile "org.mockito:mockito-core:2.7.22" 68 | testCompile "com.nhaarman:mockito-kotlin:1.4.0" 69 | testCompile 'org.amshove.kluent:kluent:1.20' 70 | 71 | compile "androidx.recyclerview:recyclerview:$androidx_version" 72 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /mapme/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jburton/dev/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /mapme/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/LatLng.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme 2 | 3 | class LatLng(var1: Double, var3: Double) { 4 | val latitude: Double 5 | val longitude: Double 6 | 7 | init { 8 | if (-180.0 <= var3 && var3 < 180.0) { 9 | this.longitude = var3 10 | } else { 11 | this.longitude = ((var3 - 180.0) % 360.0 + 360.0) % 360.0 - 180.0 12 | } 13 | this.latitude = Math.max(-90.0, Math.min(90.0, var1)) 14 | } 15 | 16 | override fun equals(other: Any?): Boolean { 17 | if (this === other) return true 18 | if (other?.javaClass != javaClass) return false 19 | 20 | other as LatLng 21 | 22 | if (latitude != other.latitude) return false 23 | if (longitude != other.longitude) return false 24 | 25 | return true 26 | } 27 | 28 | override fun hashCode(): Int { 29 | var result = latitude.hashCode() 30 | result = 31 * result + longitude.hashCode() 31 | return result 32 | } 33 | 34 | override fun toString(): String { 35 | return "LatLng(latitude=$latitude, longitude=$longitude)" 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/MapAdapterHelper.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme 2 | 3 | import android.util.Log 4 | 5 | /** 6 | * Stores adapter updates, reorders them, and dispatches them to the adapter. 7 | * 8 | * NOTE: To void confusion between updates operations and the update operation type (an item changed), 9 | * we refer to the later as change operations. 10 | * 11 | * This is needed because updates from DiffUtil cannot be applied directly to the list. 12 | * 13 | * Updates need to be reordered - reordering ensures that 'move' updates are at the end of the list. 14 | * 15 | * The updates must then be applied in 2 passes. 16 | * 17 | * The first pass does not make any structural changes to the list. This allows change operations 18 | * to occur on the correct item. 19 | * 20 | * Consider the following update list: 21 | * 22 | * ADD(0,1) 23 | * CHANGE(1,1) 24 | * 25 | * This update list is the result of changing the first item in the list, and adding a new one at the start. 26 | * If all updates were applied sequentially, the change operation would occur on the 2nd item of the 27 | * original list, rather than the first. 28 | * 29 | * To solve this, updates are applied in the following sequence: 30 | * 31 | * 1. First pass - marks and annotations with changes as needing an update 32 | * 2. Second pass - structural updates such as REMOVE, and MOVE are applied, placeholders for ADD operations are created 33 | * 3. Third pass - annotations marked as needing updates are updated, placeholders are replaced with real annotations 34 | * 35 | */ 36 | class MapAdapterHelper(val callbacks: MapAdapterHelperCallback, val debug: Boolean = false) : OpReorderer.Callback { 37 | 38 | private val TAG = "MapAdapterHelper" 39 | 40 | internal val opReorder: OpReorderer by lazy { 41 | OpReorderer(this) 42 | } 43 | 44 | private val updates: ArrayList = ArrayList() 45 | 46 | /** 47 | * @return True if updates should be processed. 48 | */ 49 | fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?): Boolean { 50 | if (itemCount < 1) { 51 | return false 52 | } 53 | updates.add(UpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload)) 54 | return updates.size == 1 55 | } 56 | 57 | /** 58 | * @return True if updates should be processed. 59 | */ 60 | fun onItemRangeInserted(positionStart: Int, itemCount: Int): Boolean { 61 | if (itemCount < 1) { 62 | return false 63 | } 64 | updates.add(UpdateOp(UpdateOp.ADD, positionStart, itemCount, null)) 65 | return updates.size == 1 66 | } 67 | 68 | /** 69 | * @return True if updates should be processed. 70 | */ 71 | fun onItemRangeRemoved(positionStart: Int, itemCount: Int): Boolean { 72 | if (itemCount < 1) { 73 | return false 74 | } 75 | updates.add(UpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null)) 76 | return updates.size == 1 77 | } 78 | 79 | /** 80 | * @return True if updates should be processed. 81 | */ 82 | fun onItemRangeMoved(from: Int, to: Int, itemCount: Int): Boolean { 83 | if (from == to) { 84 | return false 85 | } 86 | if (itemCount != 1) { 87 | throw IllegalArgumentException("Moving more than 1 item is not supported yet") 88 | } 89 | updates.add(UpdateOp(UpdateOp.MOVE, from, to, null)) 90 | return updates.size == 1 91 | } 92 | 93 | /** 94 | * Reorders adapter updates, dispatches updates and offsets positions 95 | */ 96 | fun dispatch() { 97 | opReorder.reorderOps(updates) 98 | 99 | if (this.debug) { 100 | Log.d(TAG, updates.toString()) 101 | } 102 | 103 | dispatchFirstPass() 104 | dispatchSecondPass() 105 | 106 | updates.clear() 107 | } 108 | 109 | fun dispatchSecondPass() { 110 | updates.forEach { 111 | when (it.cmd) { 112 | UpdateOp.ADD -> { 113 | callbacks.offsetPositionsForAdd(it.positionStart, it.itemCount) 114 | callbacks.dispatchUpdate(it) 115 | } 116 | UpdateOp.REMOVE -> { 117 | callbacks.dispatchUpdate(it) 118 | callbacks.offsetPositionsForRemove(it.positionStart, it.itemCount) 119 | } 120 | UpdateOp.MOVE -> { 121 | callbacks.dispatchUpdate(it) 122 | callbacks.offsetPositionsForMove(it.positionStart, it.itemCount) 123 | } 124 | UpdateOp.UPDATE -> { 125 | callbacks.dispatchUpdate(it) 126 | } 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * Dispatching the first pass updates annotation state, rather than making structural changes 133 | */ 134 | fun dispatchFirstPass() { 135 | updates.filter { it.cmd == UpdateOp.UPDATE } 136 | .forEach { op -> 137 | callbacks.markAnnotationsUpdated(op.positionStart, op.itemCount) 138 | } 139 | } 140 | 141 | interface MapAdapterHelperCallback { 142 | fun offsetPositionsForRemove(positionStart: Int, itemCount: Int) 143 | fun dispatchUpdate(update: UpdateOp) 144 | fun offsetPositionsForAdd(positionStart: Int, itemCount: Int) 145 | fun offsetPositionsForMove(from: Int, to: Int) 146 | fun markAnnotationsUpdated(positionStart: Int, itemCount: Int) 147 | } 148 | 149 | override fun obtainUpdateOp(cmd: Int, startPosition: Int, itemCount: Int, payload: Any?): UpdateOp { 150 | return UpdateOp(cmd, startPosition, itemCount, payload) 151 | } 152 | 153 | override fun recycleUpdateOp(op: UpdateOp?) { 154 | } 155 | 156 | fun hasPendingUpdates(): Boolean { 157 | return this.updates.isNotEmpty() 158 | } 159 | 160 | fun clearPendingUpdates() { 161 | this.updates.clear() 162 | } 163 | } 164 | 165 | -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/MapMeAdapter.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme 2 | 3 | import android.content.Context 4 | import androidx.annotation.RestrictTo 5 | import androidx.annotation.VisibleForTesting 6 | import android.util.Log 7 | import android.view.View 8 | import androidx.recyclerview.widget.ListUpdateCallback 9 | import androidx.recyclerview.widget.RecyclerView 10 | import nz.co.trademe.mapme.annotations.* 11 | 12 | const val TAG = "MapMeAdapter" 13 | const val NO_POSITION = -1 14 | 15 | abstract class MapMeAdapter(var context: Context, var factory: AnnotationFactory) : MapAdapterHelper.MapAdapterHelperCallback, ListUpdateCallback { 16 | 17 | var mapView: View? = null 18 | var map: MapType? = null 19 | var annotations: ArrayList = ArrayList() 20 | var annotationClickListener: OnMapAnnotationClickListener? = null 21 | var infoWindowClickListener: OnInfoWindowClickListener? = null 22 | 23 | internal var debug = false 24 | 25 | internal val observer = MapMeDataObserver() 26 | private val registeredObservers = arrayListOf() 27 | 28 | internal val mapAdapterHelper: MapAdapterHelper by lazy { 29 | MapAdapterHelper(this, this.debug) 30 | } 31 | 32 | abstract fun onCreateAnnotation(factory: AnnotationFactory<@JvmSuppressWildcards MapType>, position: Int, annotationType: Int): MapAnnotation 33 | 34 | abstract fun onBindAnnotation(annotation: MapAnnotation, position: Int, payload: Any?) 35 | 36 | abstract fun getItemCount(): Int 37 | 38 | fun attach(mapView: View, map: MapType) { 39 | this.map = map 40 | this.mapView = mapView 41 | this.factory.setOnMarkerClickListener(map, { marker -> notifyAnnotatedMarkerClicked(marker) }) 42 | this.factory.setOnInfoWindowClickListener(map, { marker -> notifyInfowWindowClicked(marker) }) 43 | } 44 | 45 | //default implementation 46 | open fun getItemAnnotationType(position: Int): Int { 47 | return 0 48 | } 49 | 50 | fun enableDebugLogging() { 51 | this.debug = true 52 | } 53 | 54 | open fun setOnAnnotationClickListener(listener: OnMapAnnotationClickListener) { 55 | this.annotationClickListener = listener 56 | } 57 | 58 | open fun setOnInfoWindowClickListener(listener: OnInfoWindowClickListener) { 59 | this.infoWindowClickListener = listener 60 | } 61 | 62 | override fun onChanged(position: Int, count: Int, payload: Any?) { 63 | observer.onItemRangeChanged(position, count, payload) 64 | } 65 | 66 | override fun onMoved(fromPosition: Int, toPosition: Int) { 67 | observer.onItemRangeMoved(fromPosition, toPosition, 1) 68 | } 69 | 70 | override fun onInserted(position: Int, count: Int) { 71 | observer.onItemRangeInserted(position, count) 72 | } 73 | 74 | override fun onRemoved(position: Int, count: Int) { 75 | observer.onItemRangeRemoved(position, count) 76 | } 77 | 78 | fun registerObserver(observer: RecyclerView.AdapterDataObserver) { 79 | registeredObservers.add(observer) 80 | } 81 | 82 | fun unregisterObserver(observer: RecyclerView.AdapterDataObserver) { 83 | registeredObservers.remove(observer) 84 | } 85 | 86 | /** 87 | * Internally creates an annotation for the given position. 88 | */ 89 | private fun createAnnotation(position: Int) { 90 | val annotationType = getItemAnnotationType(position) 91 | val annotation = onCreateAnnotation(this.factory, position, annotationType) 92 | annotation.position = position 93 | 94 | map?.let { 95 | annotation.addToMap(map!!, context) 96 | annotations.add(annotation) 97 | onBindAnnotation(annotation, position, null) 98 | 99 | onAnnotationAdded(annotation) 100 | } 101 | 102 | } 103 | 104 | open fun onAnnotationAdded(annotation: MapAnnotation) { 105 | 106 | } 107 | 108 | /** 109 | * Called after all DiffUtil updates have been dispatched. 110 | * 111 | * Removes any placeholders and replaces them with real annotations 112 | */ 113 | private fun applyUpdates() { 114 | //update old annotations 115 | this.annotations.forEach { 116 | if (it.isDirty) { 117 | updateAnnotation(it.position, null) 118 | it.isDirty = false 119 | } 120 | } 121 | 122 | //create new annotations 123 | ArrayList(this.annotations).forEach { 124 | if (it.placeholder) { 125 | this.annotations.remove(it) 126 | createAnnotation(it.position) 127 | } 128 | } 129 | 130 | } 131 | 132 | /** 133 | * Removes an annotation at the given position 134 | */ 135 | private fun removeAnnotation(position: Int) { 136 | val map = map ?: return 137 | val annotation = findAnnotationForPosition(position) 138 | 139 | if (annotation != null) { 140 | annotation.removeFromMap(map, context) 141 | annotations.remove(annotation) 142 | } else { 143 | Log.d(TAG, "annotation not found") 144 | } 145 | } 146 | 147 | /** 148 | * Updates an annotation at the given position 149 | */ 150 | private fun updateAnnotation(position: Int, payload: Any?) { 151 | val annotation = findAnnotationForPosition(position) 152 | 153 | annotation?.let { 154 | onBindAnnotation(annotation, position, payload) 155 | } 156 | } 157 | 158 | internal fun findAnnotationForPosition(position: Int): MapAnnotation? { 159 | return annotations.find { it.position == position } 160 | } 161 | 162 | fun onItemsInserted(positionStart: Int, itemCount: Int) { 163 | for (position in positionStart until positionStart + itemCount) { 164 | this.annotations.add(Placeholder().apply { this.position = position }) 165 | } 166 | } 167 | 168 | fun onItemsMoved(positionStart: Int, itemCount: Int, payload: Any?) { 169 | 170 | } 171 | 172 | fun onItemsRemoved(positionStart: Int, itemCount: Int) { 173 | for (position in positionStart until positionStart + itemCount) { 174 | removeAnnotation(position) 175 | } 176 | } 177 | 178 | fun onItemsChanged(positionStart: Int, itemCount: Int, payload: Any?) { 179 | //don't do anything here. Annotations have already been marked as updated, 180 | //and will be updated at the end of the 'layout' 181 | } 182 | 183 | /** 184 | * Notify any registered observers that the data set has changed. 185 | * 186 | *

There are two different classes of data change events, item changes and structural 187 | * changes. Item changes are when a single item has its data updated but no positional 188 | * changes have occurred. Structural changes are when items are inserted, removed or moved 189 | * within the data set.

190 | * 191 | *

This event does not specify what about the data set has changed, forcing 192 | * any observers to assume that all existing items and structure may no longer be valid. 193 | * LayoutManagers will be forced to fully rebind and relayout all visible views.

194 | * 195 | *

RecyclerView will attempt to synthesize visible structural change events 196 | * for adapters that report that they have {@link #hasStableIds() stable IDs} when 197 | * this method is used. This can help for the purposes of animation and visual 198 | * object persistence but individual item views will still need to be rebound 199 | * and relaid out.

200 | * 201 | *

If you are writing an adapter it will always be more efficient to use the more 202 | * specific change events if you can. Rely on notifyDataSetChanged() 203 | * as a last resort.

204 | * 205 | * @see #notifyItemChanged(int) 206 | * @see #notifyItemInserted(int) 207 | * @see #notifyItemRemoved(int) 208 | * @see #notifyItemRangeChanged(int, int) 209 | * @see #notifyItemRangeInserted(int, int) 210 | * @see #notifyItemRangeRemoved(int, int) 211 | */ 212 | fun notifyDataSetChanged() { 213 | val map = map ?: return 214 | 215 | onRemoved(0, annotations.size) 216 | 217 | factory.clear(map) 218 | annotations.clear() 219 | mapAdapterHelper.clearPendingUpdates() 220 | 221 | onInserted(0, getItemCount()) 222 | } 223 | 224 | /** 225 | * Notify any registered observers that the item at position has changed. 226 | * Equivalent to calling notifyItemChanged(position, null);. 227 | * 228 | *

This is an item change event, not a structural change event. It indicates that any 229 | * reflection of the data at position is out of date and should be updated. 230 | * The item at position retains the same identity.

231 | * 232 | * @param position Position of the item that has changed 233 | * 234 | * @see #notifyItemRangeChanged(int, int) 235 | */ 236 | fun notifyItemChanged(position: Int) { 237 | notifyItemChanged(position, null) 238 | } 239 | 240 | /** 241 | * Notify any registered observers that the item at position has changed with an 242 | * optional payload object. 243 | * 244 | *

This is an item change event, not a structural change event. It indicates that any 245 | * reflection of the data at position is out of date and should be updated. 246 | * The item at position retains the same identity. 247 | *

248 | * 249 | *

250 | * Client can optionally pass a payload for partial change. These payloads will be merged 251 | * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the 252 | * item is already represented by a ViewHolder and it will be rebound to the same 253 | * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing 254 | * payloads on that item and prevent future payload until 255 | * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume 256 | * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not 257 | * attached, the payload will be simply dropped. 258 | * 259 | * @param position Position of the item that has changed 260 | * @param payload Optional parameter, use null to identify a "full" update 261 | * 262 | * @see #notifyItemRangeChanged(int, int) 263 | */ 264 | fun notifyItemChanged(position: Int, payload: Any?) { 265 | observer.onItemRangeChanged(position, 1, payload) 266 | } 267 | 268 | /** 269 | * Notify any registered observers that the item reflected at position 270 | * has been newly inserted. The item previously at position is now at 271 | * position position + 1. 272 | * 273 | *

This is a structural change event. Representations of other existing items in the 274 | * data set are still considered up to date and will not be rebound, though their 275 | * positions may be altered.

276 | * 277 | * @param position Position of the newly inserted item in the data set 278 | * 279 | * @see #notifyItemRangeInserted(int, int) 280 | */ 281 | fun notifyItemInserted(position: Int) { 282 | observer.onItemRangeInserted(position, 1) 283 | } 284 | 285 | /** 286 | * Notify any registered observers that the item previously located at position 287 | * has been removed from the data set. The items previously located at and after 288 | * position may now be found at oldPosition - 1. 289 | * 290 | *

This is a structural change event. Representations of other existing items in the 291 | * data set are still considered up to date and will not be rebound, though their positions 292 | * may be altered.

293 | * 294 | * @param position Position of the item that has now been removed 295 | * 296 | * @see #notifyItemRangeRemoved(int, int) 297 | */ 298 | fun notifyItemRemoved(position: Int) { 299 | observer.onItemRangeRemoved(position, 1) 300 | } 301 | 302 | /** 303 | * Notify any registered observers that the currently reflected itemCount 304 | * items starting at positionStart have been newly inserted. The items 305 | * previously located at positionStart and beyond can now be found starting 306 | * at positionStart positionStart + itemCount. 307 | * 308 | *

This is a structural change event. Representations of other existing items in the 309 | * data set are still considered up to date and will not be rebound, though their positions 310 | * may be altered.

311 | * 312 | * @param positionStart Position of the first item that was inserted 313 | * @param itemCount Number of items inserted 314 | * 315 | * @see #notifyItemInserted(int) 316 | */ 317 | fun notifyItemRangeInserted(positionStart: Int, itemCount: Int) { 318 | for (position in positionStart until positionStart + itemCount) { 319 | notifyItemInserted(position) 320 | } 321 | } 322 | 323 | /** 324 | * Notify any registered observers that the item previously located at positionStart 325 | * has been removed from the data set. The items previously located at and after 326 | * positionStart may now be found at oldPosition - 1. 327 | * 328 | *

This is a structural change event. Representations of other existing items in the 329 | * data set are still considered up to date and will not be rebound, though their positions 330 | * may be altered.

331 | * 332 | * @param positionStart Position of the item that has now been removed 333 | * 334 | * @see #notifyItemRangeRemoved(int, int) 335 | */ 336 | fun notifyItemRangeRemoved(positionStart: Int, itemCount: Int) { 337 | for (position in positionStart + itemCount downTo positionStart) { 338 | notifyItemRemoved(position) 339 | } 340 | } 341 | 342 | 343 | /** 344 | * Notify any registered observers that the item at positionStart has changed. 345 | * Equivalent to calling notifyItemChanged(positionStart, null);. 346 | * 347 | *

This is an item change event, not a structural change event. It indicates that any 348 | * reflection of the data at positionStart is out of date and should be updated. 349 | * The item at positionStart retains the same identity.

350 | * 351 | * @param positionStart Position of the item that has changed 352 | * 353 | * @see #notifyItemRangeChanged(int, int) 354 | */ 355 | fun notifyItemRangeChanged(positionStart: Int, itemCount: Int) { 356 | notifyItemRangeChanged(positionStart, itemCount, null) 357 | } 358 | 359 | /** 360 | * Notify any registered observers that the item at positionStart has changed with an 361 | * optional payload object. 362 | * 363 | *

This is an item change event, not a structural change event. It indicates that any 364 | * reflection of the data at positionStart is out of date and should be updated. 365 | * The item at positionStart retains the same identity. 366 | *

367 | * 368 | *

369 | * Client can optionally pass a payload for partial change. These payloads will be merged 370 | * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the 371 | * item is already represented by a ViewHolder and it will be rebound to the same 372 | * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing 373 | * payloads on that item and prevent future payload until 374 | * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume 375 | * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not 376 | * attached, the payload will be simply dropped. 377 | * 378 | * @param positionStart Position of the item that has changed 379 | * @param payload Optional parameter, use null to identify a "full" update 380 | * 381 | * @see #notifyItemRangeChanged(int, int) 382 | */ 383 | fun notifyItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { 384 | for (position in positionStart until positionStart + itemCount) { 385 | notifyItemChanged(position, payload) 386 | } 387 | } 388 | 389 | /** 390 | * Notify any registered observers that the item reflected at `fromPosition` 391 | * has been moved to `toPosition`. 392 | 393 | * 394 | * This is a structural change event. Representations of other existing items in the 395 | * data set are still considered up to date and will not be rebound, though their 396 | * positions may be altered. 397 | 398 | * @param fromPosition Previous position of the item. 399 | * * 400 | * @param toPosition New position of the item. 401 | */ 402 | fun notifyItemMoved(fromPosition: Int, toPosition: Int) { 403 | observer.onItemRangeMoved(fromPosition, toPosition, 1) 404 | mapAdapterHelper.dispatch() 405 | } 406 | 407 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 408 | fun notifyAnnotatedMarkerClicked(marker: Any): Boolean { 409 | val clickListener = annotationClickListener ?: return false 410 | 411 | val annotation = annotations.find { it.annotatesObject(marker) } 412 | 413 | if (annotation != null) { 414 | return clickListener.onMapAnnotationClick(annotation) 415 | } else { 416 | Log.e("MapMeAdapter", "Unable to find an annotation that annotates the marker") 417 | } 418 | return false 419 | } 420 | 421 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 422 | fun notifyInfowWindowClicked(marker: Any): Boolean { 423 | val clickListener = infoWindowClickListener ?: return false 424 | 425 | val annotation = annotations.find { it.annotatesObject(marker) } 426 | 427 | if (annotation != null) { 428 | return clickListener.onInfoWindowClick(annotation) 429 | } else { 430 | Log.e("MapMeAdapter", "Unable to find an annotation that annotates the marker") 431 | } 432 | return false 433 | } 434 | 435 | /** 436 | * Marks an annotation as requiring an update (isDirty). The annotations will be updated 437 | * at the end of the 'layout pass' 438 | */ 439 | override fun markAnnotationsUpdated(positionStart: Int, itemCount: Int) { 440 | annotations.forEach { annotation -> 441 | val position = annotation.position 442 | if (position >= positionStart && position < positionStart + itemCount) { 443 | annotation.isDirty = true 444 | } 445 | } 446 | } 447 | 448 | override fun offsetPositionsForAdd(positionStart: Int, itemCount: Int) { 449 | annotations.forEach { annotation -> 450 | val position = annotation.position 451 | 452 | if (position >= positionStart) { 453 | offsetPosition(annotation, itemCount) 454 | } 455 | } 456 | } 457 | 458 | override fun offsetPositionsForRemove(positionStart: Int, itemCount: Int) { 459 | val positionEnd = positionStart + itemCount 460 | 461 | annotations.forEach { annotation -> 462 | if (annotation.position >= positionEnd) { 463 | offsetPosition(annotation, -itemCount) 464 | } else if (annotation.position >= positionStart) { 465 | offsetPosition(annotation, -itemCount) 466 | } 467 | } 468 | } 469 | 470 | private fun offsetPosition(annotation: MapAnnotation, offset: Int) { 471 | annotation.position += offset 472 | } 473 | 474 | override fun offsetPositionsForMove(from: Int, to: Int) { 475 | val start: Int 476 | val end: Int 477 | val inBetweenOffset: Int 478 | if (from < to) { 479 | start = from 480 | end = to 481 | inBetweenOffset = -1 482 | } else { 483 | start = to 484 | end = from 485 | inBetweenOffset = 1 486 | } 487 | 488 | annotations 489 | .filterNot { annotation -> 490 | annotation.position < start || annotation.position > end 491 | } 492 | .forEach { annotation -> 493 | val position = annotation.position 494 | if (position == from) { 495 | offsetPosition(annotation, to - from) 496 | } else { 497 | offsetPosition(annotation, inBetweenOffset) 498 | } 499 | } 500 | } 501 | 502 | internal val updateChildViewsRunnable: Runnable = Runnable { 503 | consumePendingUpdateOperations() 504 | } 505 | 506 | internal fun consumePendingUpdateOperations() { 507 | if (!mapAdapterHelper.hasPendingUpdates()) { 508 | return 509 | } 510 | 511 | mapAdapterHelper.dispatch() 512 | 513 | //after all updates have been dispatched, fill in any placeholder annotations 514 | //with new annotations 515 | applyUpdates() 516 | } 517 | 518 | @VisibleForTesting 519 | open internal fun triggerUpdateProcessor(runnable: Runnable) { 520 | mapView?.post(runnable) 521 | } 522 | 523 | inner class MapMeDataObserver : RecyclerView.AdapterDataObserver() { 524 | 525 | override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { 526 | if (mapAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) { 527 | triggerUpdateProcessor(updateChildViewsRunnable) 528 | } 529 | 530 | registeredObservers.forEach { it.onItemRangeChanged(positionStart, itemCount, payload) } 531 | } 532 | 533 | override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { 534 | if (mapAdapterHelper.onItemRangeInserted(positionStart, itemCount)) { 535 | triggerUpdateProcessor(updateChildViewsRunnable) 536 | } 537 | 538 | registeredObservers.forEach { it.onItemRangeInserted(positionStart, itemCount) } 539 | } 540 | 541 | override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { 542 | if (mapAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) { 543 | triggerUpdateProcessor(updateChildViewsRunnable) 544 | } 545 | 546 | registeredObservers.forEach { it.onItemRangeRemoved(positionStart, itemCount) } 547 | } 548 | 549 | override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { 550 | if (mapAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) { 551 | triggerUpdateProcessor(updateChildViewsRunnable) 552 | } 553 | 554 | registeredObservers.forEach { it.onItemRangeMoved(fromPosition, toPosition, itemCount) } 555 | } 556 | } 557 | 558 | override fun dispatchUpdate(update: UpdateOp) { 559 | when (update.cmd) { 560 | UpdateOp.ADD -> onItemsInserted(update.positionStart, update.itemCount) 561 | UpdateOp.REMOVE -> onItemsRemoved(update.positionStart, update.itemCount) 562 | UpdateOp.UPDATE -> onItemsChanged(update.positionStart, update.itemCount, update.payload) 563 | UpdateOp.MOVE -> onItemsMoved(update.positionStart, update.itemCount, update.payload) 564 | } 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/OpReorderer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package nz.co.trademe.mapme; 18 | 19 | import java.util.List; 20 | 21 | import static nz.co.trademe.mapme.UpdateOp.ADD; 22 | import static nz.co.trademe.mapme.UpdateOp.MOVE; 23 | import static nz.co.trademe.mapme.UpdateOp.REMOVE; 24 | import static nz.co.trademe.mapme.UpdateOp.UPDATE; 25 | 26 | class OpReorderer { 27 | 28 | final Callback mCallback; 29 | 30 | public OpReorderer(Callback callback) { 31 | mCallback = callback; 32 | } 33 | 34 | void reorderOps(List ops) { 35 | // since move operations breaks continuity, their effects on ADD/RM are hard to handle. 36 | // we push them to the end of the list so that they can be handled easily. 37 | int badMove; 38 | while ((badMove = getLastMoveOutOfOrder(ops)) != -1) { 39 | swapMoveOp(ops, badMove, badMove + 1); 40 | } 41 | } 42 | 43 | private void swapMoveOp(List list, int badMove, int next) { 44 | final UpdateOp moveOp = list.get(badMove); 45 | final UpdateOp nextOp = list.get(next); 46 | switch (nextOp.cmd) { 47 | case REMOVE: 48 | swapMoveRemove(list, badMove, moveOp, next, nextOp); 49 | break; 50 | case ADD: 51 | swapMoveAdd(list, badMove, moveOp, next, nextOp); 52 | break; 53 | case UPDATE: 54 | swapMoveUpdate(list, badMove, moveOp, next, nextOp); 55 | break; 56 | } 57 | } 58 | 59 | void swapMoveRemove(List list, int movePos, UpdateOp moveOp, 60 | int removePos, UpdateOp removeOp) { 61 | UpdateOp extraRm = null; 62 | // check if move is nulled out by remove 63 | boolean revertedMove = false; 64 | final boolean moveIsBackwards; 65 | 66 | if (moveOp.positionStart < moveOp.itemCount) { 67 | moveIsBackwards = false; 68 | if (removeOp.positionStart == moveOp.positionStart 69 | && removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) { 70 | revertedMove = true; 71 | } 72 | } else { 73 | moveIsBackwards = true; 74 | if (removeOp.positionStart == moveOp.itemCount + 1 && 75 | removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) { 76 | revertedMove = true; 77 | } 78 | } 79 | 80 | // going in reverse, first revert the effect of add 81 | if (moveOp.itemCount < removeOp.positionStart) { 82 | removeOp.positionStart--; 83 | } else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) { 84 | // move is removed. 85 | removeOp.itemCount --; 86 | moveOp.cmd = REMOVE; 87 | moveOp.itemCount = 1; 88 | if (removeOp.itemCount == 0) { 89 | list.remove(removePos); 90 | mCallback.recycleUpdateOp(removeOp); 91 | } 92 | // no need to swap, it is already a remove 93 | return; 94 | } 95 | 96 | // now affect of add is consumed. now apply effect of first remove 97 | if (moveOp.positionStart <= removeOp.positionStart) { 98 | removeOp.positionStart++; 99 | } else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) { 100 | final int remaining = removeOp.positionStart + removeOp.itemCount 101 | - moveOp.positionStart; 102 | extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining, null); 103 | removeOp.itemCount = moveOp.positionStart - removeOp.positionStart; 104 | } 105 | 106 | // if effects of move is reverted by remove, we are done. 107 | if (revertedMove) { 108 | list.set(movePos, removeOp); 109 | list.remove(removePos); 110 | mCallback.recycleUpdateOp(moveOp); 111 | return; 112 | } 113 | 114 | // now find out the new locations for move actions 115 | if (moveIsBackwards) { 116 | if (extraRm != null) { 117 | if (moveOp.positionStart > extraRm.positionStart) { 118 | moveOp.positionStart -= extraRm.itemCount; 119 | } 120 | if (moveOp.itemCount > extraRm.positionStart) { 121 | moveOp.itemCount -= extraRm.itemCount; 122 | } 123 | } 124 | if (moveOp.positionStart > removeOp.positionStart) { 125 | moveOp.positionStart -= removeOp.itemCount; 126 | } 127 | if (moveOp.itemCount > removeOp.positionStart) { 128 | moveOp.itemCount -= removeOp.itemCount; 129 | } 130 | } else { 131 | if (extraRm != null) { 132 | if (moveOp.positionStart >= extraRm.positionStart) { 133 | moveOp.positionStart -= extraRm.itemCount; 134 | } 135 | if (moveOp.itemCount >= extraRm.positionStart) { 136 | moveOp.itemCount -= extraRm.itemCount; 137 | } 138 | } 139 | if (moveOp.positionStart >= removeOp.positionStart) { 140 | moveOp.positionStart -= removeOp.itemCount; 141 | } 142 | if (moveOp.itemCount >= removeOp.positionStart) { 143 | moveOp.itemCount -= removeOp.itemCount; 144 | } 145 | } 146 | 147 | list.set(movePos, removeOp); 148 | if (moveOp.positionStart != moveOp.itemCount) { 149 | list.set(removePos, moveOp); 150 | } else { 151 | list.remove(removePos); 152 | } 153 | if (extraRm != null) { 154 | list.add(movePos, extraRm); 155 | } 156 | } 157 | 158 | private void swapMoveAdd(List list, int move, UpdateOp moveOp, int add, 159 | UpdateOp addOp) { 160 | int offset = 0; 161 | // going in reverse, first revert the effect of add 162 | if (moveOp.itemCount < addOp.positionStart) { 163 | offset--; 164 | } 165 | if (moveOp.positionStart < addOp.positionStart) { 166 | offset++; 167 | } 168 | if (addOp.positionStart <= moveOp.positionStart) { 169 | moveOp.positionStart += addOp.itemCount; 170 | } 171 | if (addOp.positionStart <= moveOp.itemCount) { 172 | moveOp.itemCount += addOp.itemCount; 173 | } 174 | addOp.positionStart += offset; 175 | list.set(move, addOp); 176 | list.set(add, moveOp); 177 | } 178 | 179 | void swapMoveUpdate(List list, int move, UpdateOp moveOp, int update, 180 | UpdateOp updateOp) { 181 | UpdateOp extraUp1 = null; 182 | UpdateOp extraUp2 = null; 183 | // going in reverse, first revert the effect of add 184 | if (moveOp.itemCount < updateOp.positionStart) { 185 | updateOp.positionStart--; 186 | } else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) { 187 | // moved item is updated. add an update for it 188 | updateOp.itemCount--; 189 | extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1, updateOp.payload); 190 | } 191 | // now affect of add is consumed. now apply effect of first remove 192 | if (moveOp.positionStart <= updateOp.positionStart) { 193 | updateOp.positionStart++; 194 | } else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) { 195 | final int remaining = updateOp.positionStart + updateOp.itemCount 196 | - moveOp.positionStart; 197 | extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining, 198 | updateOp.payload); 199 | updateOp.itemCount -= remaining; 200 | } 201 | list.set(update, moveOp); 202 | if (updateOp.itemCount > 0) { 203 | list.set(move, updateOp); 204 | } else { 205 | list.remove(move); 206 | mCallback.recycleUpdateOp(updateOp); 207 | } 208 | if (extraUp1 != null) { 209 | list.add(move, extraUp1); 210 | } 211 | if (extraUp2 != null) { 212 | list.add(move, extraUp2); 213 | } 214 | } 215 | 216 | private int getLastMoveOutOfOrder(List list) { 217 | boolean foundNonMove = false; 218 | for (int i = list.size() - 1; i >= 0; i--) { 219 | final UpdateOp op1 = list.get(i); 220 | if (op1.cmd == MOVE) { 221 | if (foundNonMove) { 222 | return i; 223 | } 224 | } else { 225 | foundNonMove = true; 226 | } 227 | } 228 | return -1; 229 | } 230 | 231 | static interface Callback { 232 | 233 | UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload); 234 | 235 | void recycleUpdateOp(UpdateOp op); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/UpdateOp.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme; 2 | 3 | public class UpdateOp { 4 | 5 | static final int ADD = 1; 6 | 7 | static final int REMOVE = 1 << 1; 8 | 9 | static final int UPDATE = 1 << 2; 10 | 11 | static final int MOVE = 1 << 3; 12 | 13 | static final int POOL_SIZE = 30; 14 | 15 | int cmd; 16 | 17 | int positionStart; 18 | 19 | Object payload; 20 | 21 | // holds the target positionStart if this is a MOVE 22 | int itemCount; 23 | 24 | UpdateOp(int cmd, int positionStart, int itemCount, Object payload) { 25 | this.cmd = cmd; 26 | this.positionStart = positionStart; 27 | this.itemCount = itemCount; 28 | this.payload = payload; 29 | } 30 | 31 | String cmdToString() { 32 | switch (cmd) { 33 | case ADD: 34 | return "add"; 35 | case REMOVE: 36 | return "remove"; 37 | case UPDATE: 38 | return "update"; 39 | case MOVE: 40 | return "move"; 41 | } 42 | return "??"; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return cmdToString() + "(" + positionStart + ", " + itemCount + ")"; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) { 53 | return true; 54 | } 55 | if (o == null || getClass() != o.getClass()) { 56 | return false; 57 | } 58 | 59 | UpdateOp op = (UpdateOp) o; 60 | 61 | if (cmd != op.cmd) { 62 | return false; 63 | } 64 | if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) { 65 | // reverse of this is also true 66 | if (itemCount == op.positionStart && positionStart == op.itemCount) { 67 | return true; 68 | } 69 | } 70 | if (itemCount != op.itemCount) { 71 | return false; 72 | } 73 | if (positionStart != op.positionStart) { 74 | return false; 75 | } 76 | if (payload != null) { 77 | if (!payload.equals(op.payload)) { 78 | return false; 79 | } 80 | } else if (op.payload != null) { 81 | return false; 82 | } 83 | 84 | return true; 85 | } 86 | 87 | @Override 88 | public int hashCode() { 89 | int result = cmd; 90 | result = 31 * result + positionStart; 91 | result = 31 * result + itemCount; 92 | return result; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/annotations/AnnotationFactory.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.annotations 2 | 3 | import android.graphics.Bitmap 4 | import nz.co.trademe.mapme.LatLng 5 | 6 | interface AnnotationFactory { 7 | 8 | fun createMarker(latLng: LatLng, icon: Bitmap?, title: String?): MarkerAnnotation 9 | 10 | fun clear(map: Map) 11 | 12 | fun setOnMarkerClickListener(map: Map, onClick: (marker: Any) -> Boolean) 13 | 14 | fun setOnInfoWindowClickListener(map: Map, onClick: (marker: Any) -> Boolean) 15 | 16 | } -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/annotations/MapAnnotation.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.annotations 2 | 3 | import android.content.Context 4 | import nz.co.trademe.mapme.NO_POSITION 5 | import java.io.Serializable 6 | 7 | abstract class MapAnnotation : Serializable { 8 | 9 | var position = NO_POSITION 10 | var placeholder = false 11 | internal var isDirty = false 12 | 13 | abstract fun annotatesObject(nativeObject: Any): Boolean 14 | 15 | abstract fun addToMap(map: Any, context: Context) 16 | 17 | abstract fun removeFromMap(map: Any, context: Context) 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/annotations/MarkerAnnotation.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.annotations 2 | 3 | import android.graphics.Bitmap 4 | import nz.co.trademe.mapme.LatLng 5 | 6 | abstract class MarkerAnnotation(latLng: LatLng, 7 | title: String? = null, 8 | icon: Bitmap? = null, 9 | zIndex: Float = 0f, 10 | alpha: Float = 1f, 11 | anchorUV: Pair = Pair(0.5f, 1.0f)) : MapAnnotation() { 12 | 13 | var latLng: LatLng = latLng 14 | set(value) { 15 | field = value 16 | onUpdatePosition(value) 17 | } 18 | 19 | var title: String? = title 20 | set(value) { 21 | field = value 22 | onUpdateTitle(value) 23 | } 24 | 25 | var icon: Bitmap? = icon 26 | set(value) { 27 | field = value 28 | onUpdateIcon(value) 29 | } 30 | 31 | var zIndex: Float = zIndex 32 | set(value) { 33 | field = value 34 | onUpdateZIndex(value) 35 | } 36 | 37 | var alpha: Float = alpha 38 | set(value) { 39 | field = value 40 | onUpdateAlpha(value) 41 | } 42 | 43 | var anchor: Pair = anchorUV 44 | set(value) { 45 | field = value 46 | onUpdateAnchor(value) 47 | } 48 | 49 | /** 50 | * Called when an icon has been set on the annotation. 51 | * 52 | * Update the native marker with the [icon] 53 | */ 54 | abstract protected fun onUpdateIcon(icon: Bitmap?) 55 | 56 | /** 57 | * Called when an title has been set on the annotation. 58 | * 59 | * Update the native marker with the [title] 60 | */ 61 | abstract protected fun onUpdateTitle(title: String?) 62 | 63 | /** 64 | * Called when a position has been set on the annotation. 65 | * 66 | * Update the native marker with the [position] 67 | */ 68 | abstract protected fun onUpdatePosition(position: LatLng) 69 | 70 | /** 71 | * Called when a zindex has been set on the annotation. 72 | * 73 | * Update the native marker with the [zindex] 74 | */ 75 | abstract protected fun onUpdateZIndex(index: Float) 76 | 77 | /** 78 | * Called when an alpha has been set on the annotation. 79 | * 80 | * Update the native marker with the [alpha] 81 | */ 82 | abstract protected fun onUpdateAlpha(alpha: Float) 83 | 84 | /** 85 | * Called when an anchor has been set on the annotation. 86 | * 87 | * Update the native marker with the [anchor] 88 | */ 89 | abstract protected fun onUpdateAnchor(anchorUV: Pair) 90 | 91 | 92 | override fun toString(): String { 93 | return "MarkerAnnotation(latLng=$latLng, title=$title, icon=$icon, zIndex=$zIndex, alpha=$alpha, anchor(U,V)=${anchor.first},${anchor.second})" 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/annotations/OnInfoWindowClickListener.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.annotations; 2 | 3 | public interface OnInfoWindowClickListener { 4 | 5 | boolean onInfoWindowClick(MapAnnotation mapAnnotationObject); 6 | 7 | } -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/annotations/OnMapAnnotationClickListener.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.annotations; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | public interface OnMapAnnotationClickListener { 6 | 7 | boolean onMapAnnotationClick(@NonNull MapAnnotation mapAnnotationObject); 8 | 9 | } -------------------------------------------------------------------------------- /mapme/src/main/java/nz/co/trademe/mapme/annotations/Placeholder.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.annotations 2 | 3 | import android.content.Context 4 | 5 | /** 6 | * This class exists as a placeholder for a map annotation. It is needed to hold a position value, 7 | * which will be manipulated as DiffUtil updates are applied. Once all updates are applied, this placeholder 8 | * will be converted into an annotation via createAnnotation. 9 | * 10 | * Without this placeholder, createAnnotation is called to early, before all DiffUtil updates 11 | * have been applied. This causes issues where createAnnotation is called with a position that is 12 | * out of bounds. 13 | */ 14 | internal class Placeholder : MapAnnotation() { 15 | 16 | init { 17 | this.placeholder = true 18 | } 19 | 20 | override fun annotatesObject(nativeObject: Any): Boolean { 21 | return false 22 | } 23 | 24 | override fun addToMap(map: Any, context: Context) { 25 | } 26 | 27 | override fun removeFromMap(map: Any, context: Context) { 28 | } 29 | } -------------------------------------------------------------------------------- /mapme/src/test/java/nz/co/trademe/mapme/AdapterHelperTest.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme 2 | 3 | import com.nhaarman.mockito_kotlin.any 4 | import com.nhaarman.mockito_kotlin.eq 5 | import com.nhaarman.mockito_kotlin.times 6 | import com.nhaarman.mockito_kotlin.verify 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.mockito.Mock 10 | import org.mockito.MockitoAnnotations 11 | 12 | class AdapterHelperTest { 13 | 14 | @Mock 15 | lateinit var callbacks: MapAdapterHelper.MapAdapterHelperCallback 16 | 17 | @Before 18 | fun before() { 19 | MockitoAnnotations.initMocks(this) 20 | } 21 | 22 | /** 23 | * Given the following list: 24 | * 25 | * Item 1, 26 | * Item 2, 27 | * Item 3, 28 | * 29 | * And the following operations: 30 | * 31 | * * Change value of field in Item at position 0 32 | * * Move item from position 0 to position 2 33 | * 34 | * The following list of updates will be produced: (See AdapterTestMove#move_change_advanced) 35 | * 36 | * update(0, 1) 37 | * move(2, 0) 38 | * 39 | * The updates should be reordered and adjusted to give the correct output, which should be: 40 | * 41 | * update(2, 0) (The update should have its position corrected to be the final position of the item) 42 | * move(2, 0) 43 | */ 44 | @Test 45 | fun testAdapterHelper() { 46 | val adapterHelper = MapAdapterHelper(callbacks, false) 47 | adapterHelper.onItemRangeChanged(0, 1, null) 48 | adapterHelper.onItemRangeMoved(2, 0, 1) 49 | 50 | adapterHelper.dispatch() 51 | 52 | 53 | verify(callbacks, times(1)).dispatchUpdate(eq(UpdateOp(UpdateOp.UPDATE, 0, 1, null))) 54 | verify(callbacks, times(1)).dispatchUpdate(eq(UpdateOp(UpdateOp.MOVE, 2, 0, null))) 55 | 56 | verify(callbacks, times(2)).dispatchUpdate(any()) 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /mapme/src/test/java/nz/co/trademe/mapme/BaseAdapterTest.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.RecyclerView 7 | import android.view.View 8 | import com.nhaarman.mockito_kotlin.spy 9 | import nz.co.trademe.mapme.util.TestAdapter 10 | import nz.co.trademe.mapme.util.TestAnnotation 11 | import nz.co.trademe.mapme.util.TestAnnotationFactory 12 | import nz.co.trademe.mapme.util.TestItem 13 | import nz.co.trademe.mapme.util.TestItemDiffCallback 14 | import nz.co.trademe.mapme.util.TestMap 15 | import nz.co.trademe.mapme.util.addItems 16 | import org.junit.Before 17 | import org.mockito.Mock 18 | import org.mockito.MockitoAnnotations 19 | 20 | @SuppressLint("NewApi") 21 | abstract class BaseAdapterTest { 22 | 23 | @Mock 24 | lateinit var context: Context 25 | 26 | @Mock 27 | lateinit var mapView: View 28 | 29 | var items = arrayListOf() 30 | 31 | lateinit var adapter: TestAdapter 32 | var adapterObserver: RecyclerView.AdapterDataObserver = spy(TestDataObserver()) 33 | 34 | @Before 35 | fun before() { 36 | MockitoAnnotations.initMocks(this) 37 | items.clear() 38 | } 39 | 40 | fun createAdapter(count: Int) { 41 | items = addItems(items, count) 42 | val factory = TestAnnotationFactory() 43 | 44 | adapter = TestAdapter(items, context, factory) 45 | resetVerification() 46 | adapter.attach(mapView, TestMap()) 47 | adapter.notifyDataSetChanged() 48 | adapter.await() 49 | adapter.resetCounts() 50 | } 51 | 52 | /** 53 | * Resets the adapter observer and adapter update/create verifications 54 | */ 55 | fun resetVerification() { 56 | adapter.unregisterObserver(adapterObserver) 57 | val observer = TestDataObserver() 58 | adapterObserver = spy(observer) 59 | adapter.registerObserver(adapterObserver) 60 | 61 | adapter.resetCounts() 62 | } 63 | 64 | /** 65 | * Waits for all asynchronous updates to complete 66 | */ 67 | fun TestAdapter.await() = phaser.arriveAndAwaitAdvance() 68 | 69 | 70 | /** 71 | * Convenience method to find the annotation with a matching position 72 | */ 73 | fun TestAdapter.annotationWithPosition(position: Int): TestAnnotation { 74 | return annotations.find { it.position == position } as TestAnnotation 75 | } 76 | 77 | open fun dispatchDiff(old: List, new: List, detectMoves: Boolean = false) : TestAdapter { 78 | val callback = TestItemDiffCallback(old, new) 79 | val diffResult = DiffUtil.calculateDiff(callback, detectMoves) 80 | 81 | items.clear() 82 | items.addAll(new) 83 | diffResult.dispatchUpdatesTo(adapter) 84 | return adapter 85 | } 86 | 87 | /** 88 | * A test observer used for verification 89 | */ 90 | open class TestDataObserver : RecyclerView.AdapterDataObserver() { 91 | override fun onChanged() { 92 | println("onChanged") 93 | } 94 | 95 | override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { 96 | super.onItemRangeRemoved(positionStart, itemCount) 97 | println("onItemRangeRemoved positionStart: $positionStart, itemCount: $itemCount") 98 | } 99 | 100 | override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { 101 | super.onItemRangeMoved(fromPosition, toPosition, itemCount) 102 | println("onItemRangeMoved fromPosition: $fromPosition, toPosition: $toPosition, itemCount: $itemCount") 103 | } 104 | 105 | override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { 106 | super.onItemRangeInserted(positionStart, itemCount) 107 | println("onItemRangeInserted positionStart: $positionStart, itemCount: $itemCount") 108 | } 109 | 110 | override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { 111 | super.onItemRangeChanged(positionStart, itemCount) 112 | println("onItemRangeChanged positionStart: $positionStart, itemCount: $itemCount") 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /mapme/src/test/java/nz/co/trademe/mapme/move/AdapterTestAdvanced.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.move 2 | 3 | import android.annotation.SuppressLint 4 | import nz.co.trademe.mapme.BaseAdapterTest 5 | import nz.co.trademe.mapme.util.addItems 6 | import nz.co.trademe.mapme.util.moveAndChangeItem 7 | import nz.co.trademe.mapme.util.moveItem 8 | import nz.co.trademe.mapme.util.shuffleItems 9 | import org.amshove.kluent.shouldBeGreaterThan 10 | import org.amshove.kluent.shouldEqual 11 | import org.amshove.kluent.shouldNotEqual 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | import org.junit.runners.Parameterized 15 | import java.util.* 16 | 17 | /** 18 | * Tests advanced change operations 19 | */ 20 | @SuppressLint("NewApi") 21 | @RunWith(Parameterized::class) 22 | class AdapterTestAdvanced(val detectMoves: Boolean) : BaseAdapterTest() { 23 | 24 | companion object { 25 | @JvmStatic 26 | @Parameterized.Parameters 27 | fun data(): Collection { 28 | return listOf(false, true) 29 | } 30 | } 31 | 32 | /** 33 | * Tests shuffling the list, but making no changes 34 | */ 35 | @Test 36 | fun shuffle() { 37 | createAdapter(10).run { resetVerification() } 38 | 39 | dispatchDiff(items, shuffleItems(items), detectMoves).await() 40 | 41 | if (detectMoves) { 42 | //when shuffling items, nothing should be created when detect moves is enabled 43 | adapter.createCount shouldEqual 0 44 | adapter.bindCount shouldEqual 0 45 | } else { 46 | //when shuffling without move detection, items should be recreated 47 | adapter.createCount shouldBeGreaterThan 0 48 | adapter.bindCount shouldBeGreaterThan 0 49 | } 50 | } 51 | 52 | /** 53 | * Tests moving an item and changing it at the same time 54 | */ 55 | @Test 56 | fun move_change() { 57 | createAdapter(3).run { resetVerification() } 58 | 59 | ArrayList(items) 60 | .run { moveAndChangeItem(this, 0, 2) } 61 | .run { dispatchDiff(items, this, detectMoves).await() } 62 | 63 | if (detectMoves) { 64 | adapter.bindCount shouldEqual 1 65 | adapter.createCount shouldEqual 0 66 | } else { 67 | adapter.bindCount shouldEqual 1 68 | adapter.createCount shouldEqual 1 69 | } 70 | 71 | //ensure this item and its annotation both have had their value changed 72 | adapter.items[2].zIndex shouldNotEqual 0f 73 | adapter.annotationWithPosition(2).zIndex shouldNotEqual 0f 74 | } 75 | 76 | /** 77 | * Tests moving and changing an item, and moving another item 78 | */ 79 | @Test 80 | fun move_change_advanced() { 81 | createAdapter(3) 82 | 83 | resetVerification() 84 | 85 | ArrayList(items) 86 | .run { moveAndChangeItem(this, 0, 2) } 87 | .run { moveItem(this, 1, 0) } 88 | .run { dispatchDiff(items, this, detectMoves).await() } 89 | 90 | if (detectMoves) { 91 | adapter.bindCount shouldEqual 1 92 | } else { 93 | adapter.bindCount shouldEqual 3 94 | } 95 | 96 | adapter.items[2].zIndex shouldEqual 1f 97 | } 98 | 99 | /** 100 | * Tests moving and changing an item, and moving another item on a larger list size 101 | */ 102 | @Test 103 | fun move_change_advanced_large() { 104 | createAdapter(0) 105 | 106 | dispatchDiff(items, addItems(items, 10), detectMoves) 107 | .run { adapter.await() } 108 | .run { resetVerification() } 109 | 110 | //change item and move to end 111 | ArrayList(items) 112 | .run { moveAndChangeItem(this, 0, items.size - 1) } 113 | .run { moveItem(this, 1, 0) } 114 | .run { dispatchDiff(items, this, true).await() } 115 | 116 | adapter.bindCount shouldEqual 1 117 | adapter.items[items.size - 1].zIndex shouldEqual 1f 118 | } 119 | 120 | } 121 | 122 | -------------------------------------------------------------------------------- /mapme/src/test/java/nz/co/trademe/mapme/simple/AdapterTestSimple.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.simple 2 | 3 | import nz.co.trademe.mapme.BaseAdapterTest 4 | import nz.co.trademe.mapme.util.addItems 5 | import nz.co.trademe.mapme.util.changeItem 6 | import nz.co.trademe.mapme.util.moveItem 7 | import nz.co.trademe.mapme.util.removeItems 8 | import org.amshove.kluent.shouldEqual 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.junit.runners.Parameterized 12 | 13 | /** 14 | * Tests simple change operations 15 | */ 16 | @android.annotation.SuppressLint("NewApi") 17 | @RunWith(Parameterized::class) 18 | class AdapterTestSimple(val detectMoves: Boolean) : BaseAdapterTest() { 19 | 20 | companion object { 21 | @JvmStatic 22 | @Parameterized.Parameters 23 | fun data(): Collection { 24 | return listOf(false, true) 25 | } 26 | } 27 | 28 | /** 29 | * Tests adding items to the list 30 | */ 31 | @Test 32 | fun addition() { 33 | createAdapter(0).run { resetVerification() } 34 | 35 | dispatchDiff(items, addItems(items, 5), detectMoves).await() 36 | 37 | adapter.annotations.size shouldEqual 5 38 | 39 | adapter.createCount shouldEqual 5 40 | adapter.bindCount shouldEqual 5 41 | } 42 | 43 | /** 44 | * Tests removing items from the list 45 | */ 46 | @Test 47 | fun removal() { 48 | createAdapter(10).run { resetVerification() } 49 | 50 | dispatchDiff(items, removeItems(items, 5), detectMoves).await() 51 | 52 | adapter.annotations.size shouldEqual 5 53 | 54 | adapter.createCount shouldEqual 0 55 | adapter.bindCount shouldEqual 0 56 | } 57 | 58 | /** 59 | * Tests changing (updating) an item in the list 60 | */ 61 | @Test 62 | fun updating() { 63 | createAdapter(10).run { resetVerification() } 64 | 65 | dispatchDiff(items, changeItem(items, 1), detectMoves).await() 66 | 67 | adapter.annotations.size shouldEqual 10 68 | 69 | //ensure item and annotation was changed 70 | 1f shouldEqual adapter.items[1].zIndex 71 | 1f shouldEqual adapter.annotationWithPosition(1).zIndex 72 | 73 | adapter.createCount shouldEqual 0 74 | adapter.bindCount shouldEqual 1 75 | } 76 | 77 | /** 78 | * Tests moving an item in the list 79 | */ 80 | @Test 81 | fun moving() { 82 | createAdapter(10).run { resetVerification() } 83 | 84 | dispatchDiff(items, moveItem(items, 0, 9), detectMoves).await() 85 | 86 | adapter.items[0].startPosition shouldEqual 1 87 | adapter.items[9].startPosition shouldEqual 0 88 | 89 | if (detectMoves) { 90 | adapter.createCount shouldEqual 0 91 | adapter.bindCount shouldEqual 0 92 | } else { 93 | adapter.createCount shouldEqual 1 94 | adapter.bindCount shouldEqual 1 95 | } 96 | } 97 | 98 | } 99 | 100 | -------------------------------------------------------------------------------- /mapme/src/test/java/nz/co/trademe/mapme/util/ItemsHelper.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.util 2 | 3 | import java.util.* 4 | 5 | fun addItems(items: ArrayList, count: Int): ArrayList { 6 | val added = ArrayList(items) 7 | 8 | for (i in 0 until count) { 9 | added.add(TestItem(startPosition = items.size + i)) 10 | } 11 | 12 | return added 13 | } 14 | 15 | fun removeItems(items: ArrayList, count: Int): ArrayList { 16 | val removed = ArrayList(items) 17 | 18 | for (i in (items.size - 1) downTo items.size - count) { 19 | removed.removeAt(i) 20 | } 21 | return removed 22 | } 23 | 24 | fun changeItem(items: ArrayList, position: Int): ArrayList { 25 | val changed = ArrayList(items) 26 | 27 | changed[position] = changed[position].copy(zIndex = 1f) 28 | 29 | return changed 30 | } 31 | 32 | fun moveItem(items: ArrayList, from: Int, to: Int): ArrayList { 33 | return ArrayList(items).apply { 34 | add(to, this.removeAt(from).copy()) 35 | } 36 | } 37 | 38 | fun moveAndChangeItem(items: ArrayList, from: Int, to: Int): ArrayList { 39 | return ArrayList(items).apply { 40 | add(to, this.removeAt(from).copy(zIndex = 1f)) 41 | } 42 | } 43 | 44 | fun shuffleItems(items: ArrayList): ArrayList { 45 | return ArrayList(items).apply { 46 | Collections.shuffle(this) 47 | } 48 | } -------------------------------------------------------------------------------- /mapme/src/test/java/nz/co/trademe/mapme/util/TestAdapter.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import nz.co.trademe.mapme.MapMeAdapter 6 | import nz.co.trademe.mapme.annotations.MapAnnotation 7 | import nz.co.trademe.mapme.annotations.AnnotationFactory 8 | import nz.co.trademe.mapme.annotations.MarkerAnnotation 9 | import java.util.* 10 | import java.util.concurrent.Phaser 11 | 12 | class TestMap 13 | 14 | open class TestAdapter(val items: List, myContext: Context, myFactory: AnnotationFactory) : MapMeAdapter(myContext, myFactory) { 15 | 16 | var createCount: Int = 0 17 | var bindCount: Int = 0 18 | 19 | @SuppressLint("NewApi") 20 | val phaser = Phaser().apply { register() } 21 | 22 | override fun onCreateAnnotation(factory: AnnotationFactory, position: Int, viewType: Int): MapAnnotation { 23 | createCount++ 24 | return TestAnnotation() 25 | } 26 | 27 | override fun onBindAnnotation(annotation: MapAnnotation, position: Int, payload: Any?) { 28 | val testData = items[position] 29 | (annotation as MarkerAnnotation).zIndex = testData.zIndex 30 | 31 | bindCount++ 32 | } 33 | 34 | override fun getItemCount(): Int { 35 | return items.size 36 | } 37 | 38 | @SuppressLint("NewApi") 39 | override fun triggerUpdateProcessor(runnable: Runnable) { 40 | phaser.register() 41 | val task = object : TimerTask() { 42 | override fun run() { 43 | runnable.run() 44 | phaser.arriveAndDeregister() 45 | } 46 | } 47 | Timer().schedule(task, 10) 48 | } 49 | 50 | fun resetCounts() { 51 | createCount = 0 52 | bindCount = 0 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /mapme/src/test/java/nz/co/trademe/mapme/util/TestAnnotation.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.util 2 | 3 | class TestAnnotation : nz.co.trademe.mapme.annotations.MarkerAnnotation(nz.co.trademe.mapme.LatLng(0.0, 0.0), "", null, 0f, 1f, Pair(0.5f, 1f)) { 4 | override fun annotatesObject(nativeObject: Any): Boolean { 5 | return false 6 | } 7 | 8 | override fun addToMap(map: Any, context: android.content.Context) { 9 | } 10 | 11 | override fun removeFromMap(map: Any, context: android.content.Context) { 12 | } 13 | 14 | override fun onUpdateIcon(icon: android.graphics.Bitmap?) { 15 | } 16 | 17 | override fun onUpdateTitle(title: String?) { 18 | } 19 | 20 | override fun onUpdatePosition(position: nz.co.trademe.mapme.LatLng) { 21 | } 22 | 23 | override fun onUpdateZIndex(index: Float) { 24 | } 25 | 26 | override fun onUpdateAlpha(alpha: Float) { 27 | } 28 | 29 | override fun onUpdateAnchor(anchorUV: Pair) { 30 | } 31 | 32 | override fun toString(): String { 33 | return "TestAnnotation( position = $position)" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mapme/src/test/java/nz/co/trademe/mapme/util/TestAnnotationFactory.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.util 2 | 3 | class TestAnnotationFactory : nz.co.trademe.mapme.annotations.AnnotationFactory { 4 | 5 | override fun createMarker(latLng: nz.co.trademe.mapme.LatLng, icon: android.graphics.Bitmap?, title: String?): nz.co.trademe.mapme.annotations.MarkerAnnotation { 6 | return TestAnnotation() 7 | } 8 | 9 | override fun clear(map: TestMap) { 10 | } 11 | 12 | override fun setOnMarkerClickListener(map: TestMap, onClick: (marker: Any) -> Boolean) { 13 | } 14 | 15 | override fun setOnInfoWindowClickListener(map: TestMap, onClick: (marker: Any) -> Boolean) { 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /mapme/src/test/java/nz/co/trademe/mapme/util/TestItem.kt: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.util 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import java.util.* 5 | 6 | data class TestItem(val id: String = UUID.randomUUID().toString(), val startPosition: Int, val zIndex: Float = 0f) 7 | 8 | class TestItemDiffCallback(private val mOldList: List, private val mNewList: List) : DiffUtil.Callback() { 9 | 10 | override fun getOldListSize(): Int { 11 | return mOldList.size 12 | } 13 | 14 | override fun getNewListSize(): Int { 15 | return mNewList.size 16 | } 17 | 18 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 19 | return mNewList[newItemPosition].id == mOldList[oldItemPosition].id 20 | } 21 | 22 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 23 | return mNewList[newItemPosition] == mOldList[oldItemPosition] 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /publish-root.gradle: -------------------------------------------------------------------------------- 1 | 2 | // Create variables with empty default values 3 | ext["signing.keyId"] = '' 4 | ext["signing.password"] = '' 5 | ext["signing.key"] = '' 6 | ext["ossrhUsername"] = '' 7 | ext["ossrhPassword"] = '' 8 | ext["sonatypeStagingProfileId"] = '' 9 | ext["snapshot"] = 'true' 10 | 11 | File secretPropsFile = project.rootProject.file('local.properties') 12 | if (secretPropsFile.exists()) { 13 | // Read local.properties file first if it exists 14 | Properties p = new Properties() 15 | new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } 16 | p.each { name, value -> ext[name] = value } 17 | } else { 18 | // Use system environment variables 19 | ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') 20 | ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') 21 | ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') 22 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') 23 | ext["signing.password"] = System.getenv('SIGNING_PASSWORD') 24 | ext["signing.key"] = System.getenv('SIGNING_KEY') 25 | ext["snapshot"] = System.getenv('SNAPSHOT') 26 | } 27 | 28 | if (Boolean.parseBoolean(snapshot)) { 29 | ext["rootVersionName"] = project.ext.version + "-SNAPSHOT" 30 | } else { 31 | ext["rootVersionName"] = project.ext.version 32 | } 33 | 34 | // Set up Sonatype repository 35 | nexusPublishing { 36 | repositories { 37 | sonatype { 38 | useStaging.set(provider { 39 | !Boolean.parseBoolean(snapshot) 40 | }) 41 | stagingProfileId = sonatypeStagingProfileId 42 | username = ossrhUsername 43 | password = ossrhPassword 44 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 45 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /publishing.gradle: -------------------------------------------------------------------------------- 1 | 2 | apply plugin: 'maven-publish' 3 | apply plugin: 'signing' 4 | apply plugin: 'org.jetbrains.dokka' 5 | 6 | task androidSourcesJar(type: Jar) { 7 | archiveClassifier.set('sources') 8 | if (project.plugins.findPlugin("com.android.library")) { 9 | 10 | // For Android libraries 11 | from android.sourceSets.main.java.srcDirs 12 | from android.sourceSets.main.kotlin.srcDirs 13 | } else { 14 | from sourceSets.main.java.srcDirs 15 | from sourceSets.main.kotlin.srcDirs 16 | } 17 | 18 | 19 | } 20 | task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { 21 | archiveClassifier.set('javadoc') 22 | from dokkaJavadoc.outputDirectory 23 | } 24 | 25 | artifacts { 26 | archives androidSourcesJar 27 | archives javadocJar 28 | } 29 | 30 | 31 | afterEvaluate { 32 | publishing { 33 | 34 | publications { 35 | release(MavenPublication) { 36 | groupId project.ext.group 37 | artifactId ARTIFACT_ID 38 | version PUBLISH_VERSION 39 | 40 | if (project.plugins.findPlugin("com.android.library")) { 41 | from components.release 42 | } else { 43 | from components.java 44 | } 45 | 46 | artifact androidSourcesJar 47 | artifact javadocJar 48 | 49 | // metadata 50 | pom { 51 | name = ARTIFACT_ID 52 | description = 'SDK' 53 | url = project.ext.url 54 | licenses { 55 | license { 56 | name = 'MapMe License' 57 | url = 'https://github.com/sabintrademe/MapMe/blob/master/LICENSE' 58 | } 59 | } 60 | developers { 61 | developer { 62 | id = 'sabinmj' 63 | name = 'Sabin Mulakukodiyan' 64 | email = 'sabin.mulakukodiyan@trademe.co.nz' 65 | } 66 | // Add all other devs here... 67 | } 68 | 69 | // Version control info - if you're using GitHub, follow the 70 | // format as seen here 71 | scm { 72 | connection = project.ext.connection 73 | developerConnection = project.ext.developerConnection 74 | url = project.ext.url 75 | 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | signing { 83 | if (rootProject.ext["signing.keyId"] && rootProject.ext["signing.key"] && rootProject.ext["signing.password"]) { 84 | useInMemoryPgpKeys( 85 | rootProject.ext["signing.keyId"], 86 | rootProject.ext["signing.key"], 87 | rootProject.ext["signing.password"], 88 | ) 89 | sign publishing.publications 90 | } 91 | } 92 | java { 93 | sourceCompatibility = JavaVersion.VERSION_1_8 94 | targetCompatibility = JavaVersion.VERSION_1_8 95 | } -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | apply from: '../common.gradle' 3 | 4 | repositories { 5 | google() 6 | jcenter() 7 | mavenLocal() 8 | } 9 | dependencies { 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | apply plugin: 'com.android.application' 15 | apply plugin: 'kotlin-android' 16 | apply from: '../common.gradle' 17 | 18 | android { 19 | compileSdkVersion compileSdk 20 | buildToolsVersion buildTools 21 | defaultConfig { 22 | applicationId "nz.co.trademe.mapme.sample" 23 | minSdkVersion 21 24 | targetSdkVersion compileSdk 25 | versionCode 1 26 | versionName "1.0" 27 | 28 | 29 | Properties properties = new Properties() 30 | def localPropertiesFile = project.rootProject.file('local.properties') 31 | if (localPropertiesFile.exists()) { 32 | properties.load(localPropertiesFile.newDataInputStream()) 33 | } 34 | 35 | if (properties.containsKey("mapboxKey") && properties.containsKey("googleMapsKey")) { 36 | project.rootProject.file('local.properties').exists() 37 | 38 | def mapboxKey = properties.getProperty('mapboxKey'); 39 | def googleMapsKey = properties.getProperty('googleMapsKey'); 40 | 41 | resValue "string", "mapbox_key", mapboxKey 42 | resValue "string", "googlemaps_key", googleMapsKey 43 | } else { 44 | resValue "string", "mapbox_key", "enter valid key" 45 | resValue "string", "googlemaps_key", "enter valid key" 46 | logger.warn("Must declare mapboxKey and googleMapsKey in local.properties") 47 | } 48 | } 49 | 50 | buildTypes { 51 | release { 52 | minifyEnabled false 53 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 54 | } 55 | } 56 | 57 | lintOptions { 58 | lintConfig new File(rootProject.projectDir, "lint.xml") 59 | } 60 | 61 | compileOptions { 62 | sourceCompatibility 1.8 63 | targetCompatibility 1.8 64 | } 65 | } 66 | 67 | repositories { 68 | google() 69 | jcenter() 70 | } 71 | 72 | dependencies { 73 | compile project(path: ':mapme') 74 | compile 'com.jakewharton.timber:timber:4.5.1' 75 | compile 'com.squareup.picasso:picasso:2.5.2' 76 | 77 | compile "androidx.appcompat:appcompat:$androidx_version" 78 | compile "com.google.android.material:material:$androidx_version" 79 | 80 | compile "com.google.android.gms:play-services-maps:$google_play_services_version" 81 | 82 | compile 'com.mapbox.mapboxsdk:mapbox-android-services:2.2.3' 83 | 84 | compile project(':googlemaps') 85 | compile project(':mapbox') 86 | 87 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 88 | } -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jburton/dev/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 18 | 19 | 22 | 23 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /sample/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/Constants.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample; 2 | 3 | public class Constants { 4 | 5 | public static final double AUCKLAND_LAT = -36.8485; 6 | public static final double AUCKLAND_LON = 174.7633; 7 | } 8 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/MarkerBottomSheet.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample; 2 | 3 | import android.content.DialogInterface; 4 | import android.os.Bundle; 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | 12 | import nz.co.trademe.mapme.annotations.MapAnnotation; 13 | import nz.co.trademe.mapme.sample.activities.MapActivity; 14 | 15 | 16 | public class MarkerBottomSheet extends BottomSheetDialogFragment { 17 | 18 | public static final String ARG_ANNOTATION = "annotation"; 19 | private MapAnnotation mapAnnotation; 20 | 21 | public static MarkerBottomSheet newInstance(@NonNull MapAnnotation annotation) { 22 | MarkerBottomSheet fragment = new MarkerBottomSheet(); 23 | Bundle bundle = new Bundle(); 24 | bundle.putSerializable(ARG_ANNOTATION, annotation); 25 | fragment.setArguments(bundle); 26 | return fragment; 27 | } 28 | 29 | @Override 30 | public void onCreate(@Nullable Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | 33 | this.mapAnnotation = (MapAnnotation) getArguments().getSerializable(ARG_ANNOTATION); 34 | } 35 | 36 | @Nullable 37 | @Override 38 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 39 | return inflater.inflate(R.layout.marker_bottom_sheet, container, false); 40 | } 41 | 42 | @Override 43 | public void onDismiss(DialogInterface dialog) { 44 | super.onDismiss(dialog); 45 | ((MapActivity) getActivity()).unselectMarker(mapAnnotation); 46 | } 47 | 48 | @Override 49 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 50 | super.onViewCreated(view, savedInstanceState); 51 | 52 | view.findViewById(R.id.remove_textview).setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick(View v) { 55 | ((MapActivity) getActivity()).removeAnnotation(mapAnnotation); 56 | dismiss(); 57 | } 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/MarkerData.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample; 2 | 3 | import nz.co.trademe.mapme.LatLng; 4 | 5 | public class MarkerData { 6 | 7 | private final LatLng latLng; 8 | private final String title; 9 | private MarkerColour markerColour = MarkerColour.RED; 10 | private boolean selected = false; 11 | 12 | public boolean isSelected() { 13 | return selected; 14 | } 15 | 16 | 17 | public void setSelected(boolean selected) { 18 | this.selected = selected; 19 | } 20 | 21 | public MarkerData(LatLng latLng, String title) { 22 | this(latLng, title, MarkerColour.RED); 23 | } 24 | 25 | public MarkerData(LatLng latLng, String title, MarkerColour colour) { 26 | this.latLng = latLng; 27 | this.title = title; 28 | this.markerColour = colour; 29 | } 30 | 31 | public LatLng getLatLng() { 32 | return latLng; 33 | } 34 | 35 | public String getTitle() { 36 | return title; 37 | } 38 | 39 | public MarkerColour getMarkerColour() { 40 | return markerColour; 41 | } 42 | 43 | public void setMarkerColour(MarkerColour markerColour) { 44 | this.markerColour = markerColour; 45 | } 46 | 47 | public enum MarkerColour { 48 | RED, 49 | GREEN, 50 | BLUE 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return "MarkerData{" + 56 | "latLng=" + latLng + 57 | ", title='" + title + '\'' + 58 | ", markerColour=" + markerColour + 59 | '}'; 60 | } 61 | 62 | @Override 63 | public boolean equals(Object o) { 64 | if (this == o) return true; 65 | if (o == null || getClass() != o.getClass()) return false; 66 | 67 | MarkerData that = (MarkerData) o; 68 | 69 | if (selected != that.selected) return false; 70 | if (latLng != null ? !latLng.equals(that.latLng) : that.latLng != null) return false; 71 | if (title != null ? !title.equals(that.title) : that.title != null) return false; 72 | return markerColour == that.markerColour; 73 | 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | int result = latLng != null ? latLng.hashCode() : 0; 79 | result = 31 * result + (title != null ? title.hashCode() : 0); 80 | result = 31 * result + (markerColour != null ? markerColour.hashCode() : 0); 81 | result = 31 * result + (selected ? 1 : 0); 82 | return result; 83 | } 84 | 85 | public MarkerData copy() { 86 | return new MarkerData(this.latLng, this.title, this.markerColour); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/SampleApplication.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample; 2 | 3 | import android.app.Application; 4 | 5 | 6 | import com.mapbox.mapboxsdk.Mapbox; 7 | 8 | import timber.log.Timber; 9 | 10 | public class SampleApplication extends Application { 11 | 12 | @Override 13 | public void onCreate() { 14 | super.onCreate(); 15 | Timber.plant(new Timber.DebugTree()); 16 | Mapbox.getInstance(this, getResources().getString(R.string.mapbox_key)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/SampleMapMeAdapter.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.drawable.BitmapDrawable; 6 | import android.graphics.drawable.Drawable; 7 | import androidx.annotation.NonNull; 8 | import androidx.core.content.ContextCompat; 9 | 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.List; 13 | 14 | import nz.co.trademe.mapme.MapMeAdapter; 15 | import nz.co.trademe.mapme.annotations.AnnotationFactory; 16 | import nz.co.trademe.mapme.annotations.MapAnnotation; 17 | import nz.co.trademe.mapme.annotations.MarkerAnnotation; 18 | 19 | public class SampleMapMeAdapter extends MapMeAdapter { 20 | 21 | private final List markers; 22 | 23 | public SampleMapMeAdapter(@NonNull Context context, @NonNull List markers, @NonNull AnnotationFactory annotationFactory) { 24 | super(context, annotationFactory); 25 | this.markers = markers; 26 | } 27 | 28 | @NotNull 29 | @Override 30 | public MapAnnotation onCreateAnnotation(@NotNull AnnotationFactory mapFactory, int position, int viewType) { 31 | MarkerData item = this.markers.get(position); 32 | return mapFactory.createMarker(item.getLatLng(), getIconBitmap(item), item.getTitle()); 33 | } 34 | 35 | @Override 36 | public void onBindAnnotation(@NotNull MapAnnotation annotation, int position, Object payload) { 37 | if (annotation instanceof MarkerAnnotation) { 38 | MarkerData item = this.markers.get(position); 39 | ((MarkerAnnotation) annotation).setIcon(getIconBitmap(item)); 40 | } 41 | } 42 | 43 | private Bitmap getIconBitmap(MarkerData item) { 44 | Drawable icon; 45 | 46 | MarkerData.MarkerColour colour = item.getMarkerColour(); 47 | 48 | if (item.isSelected()) { 49 | colour = MarkerData.MarkerColour.GREEN; 50 | } 51 | 52 | switch (colour) { 53 | case BLUE: 54 | icon = ContextCompat.getDrawable(getContext(), R.drawable.marker_blue); 55 | break; 56 | case GREEN: 57 | icon = ContextCompat.getDrawable(getContext(), R.drawable.marker_green); 58 | break; 59 | case RED: 60 | default: 61 | icon = ContextCompat.getDrawable(getContext(), R.drawable.marker_red); 62 | break; 63 | } 64 | 65 | return ((BitmapDrawable) icon).getBitmap(); 66 | } 67 | 68 | 69 | @Override 70 | public int getItemCount() { 71 | return this.markers.size(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/Util.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample; 2 | 3 | import java.util.Random; 4 | 5 | import nz.co.trademe.mapme.LatLng; 6 | 7 | public class Util { 8 | 9 | private Util() { 10 | } 11 | 12 | public static LatLng getLocationInLatLngRad(double radiusInMeters, LatLng currentLocation) { 13 | double x0 = currentLocation.getLongitude(); 14 | double y0 = currentLocation.getLatitude(); 15 | 16 | Random random = new Random(); 17 | 18 | // Convert radius from meters to degrees. 19 | double radiusInDegrees = radiusInMeters / 111320f; 20 | 21 | // Get a random distance and a random angle. 22 | double u = random.nextDouble(); 23 | double v = random.nextDouble(); 24 | double w = radiusInDegrees * Math.sqrt(u); 25 | double t = 2 * Math.PI * v; 26 | // Get the x and y delta values. 27 | double x = w * Math.cos(t); 28 | double y = w * Math.sin(t); 29 | 30 | // Compensate the x value. 31 | double new_x = x / Math.cos(Math.toRadians(y0)); 32 | 33 | double foundLatitude; 34 | double foundLongitude; 35 | 36 | foundLatitude = y0 + y; 37 | foundLongitude = x0 + new_x; 38 | 39 | LatLng copy = new LatLng(foundLatitude, foundLongitude); 40 | return copy; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/activities/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample.activities; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import androidx.recyclerview.widget.DiffUtil; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import nz.co.trademe.mapme.LatLng; 14 | import nz.co.trademe.mapme.sample.Constants; 15 | import nz.co.trademe.mapme.sample.MarkerData; 16 | import nz.co.trademe.mapme.sample.R; 17 | import nz.co.trademe.mapme.sample.Util; 18 | 19 | 20 | public abstract class BaseActivity extends AppCompatActivity { 21 | 22 | protected static final LatLng aucklandLatLng = new LatLng(Constants.AUCKLAND_LAT, Constants.AUCKLAND_LON); 23 | 24 | protected final List markers = new ArrayList<>(); 25 | 26 | protected boolean detectMoves; 27 | 28 | protected static int MARKER_COUNT = 3; 29 | 30 | public List addSampleMarkers(List markersList, int number) { 31 | return addSampleMarkers(markersList, number, MarkerData.MarkerColour.RED); 32 | } 33 | 34 | public List addSampleMarkers(List markersList, int number, MarkerData.MarkerColour colour) { 35 | List list = new ArrayList<>(markersList); 36 | for (int i = 0; i < number; i++) { 37 | list.add(new MarkerData(Util.getLocationInLatLngRad(1000, aucklandLatLng), "Marker " + (i + 1), colour)); 38 | } 39 | 40 | return list; 41 | } 42 | 43 | @Override 44 | public boolean onCreateOptionsMenu(Menu menu) { 45 | super.onCreateOptionsMenu(menu); 46 | getMenuInflater().inflate(R.menu.menu, menu); 47 | return true; 48 | } 49 | 50 | @Override 51 | public boolean onPrepareOptionsMenu(Menu menu) { 52 | menu.findItem(R.id.detect_moves_item).setChecked(this.detectMoves); 53 | return true; 54 | } 55 | 56 | @Override 57 | public boolean onOptionsItemSelected(MenuItem item) { 58 | switch (item.getItemId()) { 59 | case android.R.id.home: 60 | finish(); 61 | return true; 62 | case R.id.detect_moves_item: 63 | this.detectMoves = !this.detectMoves; 64 | item.setChecked(this.detectMoves); 65 | break; 66 | case R.id.add_10_item: 67 | List newMarkers = addSampleMarkers(this.markers, 10); 68 | onMarkersChanged(newMarkers); 69 | return true; 70 | case R.id.remove_5_item: 71 | ArrayList removedCopy = new ArrayList<>(this.markers); 72 | List random5Removed = getRandomMarkerData(5); 73 | 74 | for (MarkerData marker : random5Removed) { 75 | removedCopy.remove(marker); 76 | } 77 | 78 | onMarkersChanged(removedCopy); 79 | return true; 80 | case R.id.add_5_remove_5_item: 81 | 82 | ArrayList markerCopy = new ArrayList<>(this.markers); 83 | List random5 = getRandomMarkerData(5); 84 | 85 | for (MarkerData marker : random5) { 86 | markerCopy.remove(marker); 87 | } 88 | 89 | List finalMarkerList = addSampleMarkers(markerCopy, 5, MarkerData.MarkerColour.BLUE); 90 | onMarkersChanged(finalMarkerList); 91 | return true; 92 | 93 | case R.id.change_5_item: 94 | ArrayList markers = new ArrayList<>(this.markers); 95 | 96 | List randomMarkers = getRandomMarkerData(5); 97 | 98 | MarkerData[] markersArray = new ArrayList<>(this.markers).toArray(new MarkerData[]{}); 99 | for (MarkerData marker : randomMarkers) { 100 | MarkerData copy = marker.copy(); 101 | int index = markers.indexOf(marker); 102 | 103 | switch (marker.getMarkerColour()) { 104 | case RED: 105 | copy.setMarkerColour(MarkerData.MarkerColour.BLUE); 106 | break; 107 | case BLUE: 108 | copy.setMarkerColour(MarkerData.MarkerColour.RED); 109 | break; 110 | } 111 | 112 | markersArray[index] = copy; 113 | } 114 | 115 | onMarkersChanged(Arrays.asList(markersArray)); 116 | return true; 117 | case R.id.move_change: 118 | ArrayList changedMarkers = new ArrayList<>(this.markers); 119 | 120 | //move a marker from the start of the list to the end 121 | MarkerData marker = changedMarkers.remove(0); 122 | MarkerData copy = marker.copy(); 123 | copy.setMarkerColour(MarkerData.MarkerColour.BLUE); 124 | changedMarkers.add(copy); 125 | onMarkersChanged(changedMarkers); 126 | 127 | return true; 128 | } 129 | 130 | return false; 131 | } 132 | 133 | protected void onMarkersChanged(List newMarkers) { 134 | MarkerDiffCallback callback = new MarkerDiffCallback(this.markers, newMarkers); 135 | final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback, this.detectMoves); 136 | 137 | this.markers.clear(); 138 | this.markers.addAll(newMarkers); 139 | dispatchDiffUtilResult(diffResult); 140 | } 141 | 142 | abstract void dispatchDiffUtilResult(DiffUtil.DiffResult result); 143 | 144 | private List getRandomMarkerData(int count) { 145 | List randomMarkers = new ArrayList<>(); 146 | List markers = new ArrayList<>(this.markers); 147 | 148 | while (randomMarkers.size() < count) { 149 | Collections.shuffle(markers); 150 | if (!randomMarkers.contains(markers.get(0))) { 151 | randomMarkers.add(markers.get(0)); 152 | } 153 | } 154 | 155 | return randomMarkers; 156 | } 157 | 158 | 159 | public static class MarkerDiffCallback extends DiffUtil.Callback { 160 | private List mOldList; 161 | private List mNewList; 162 | 163 | public MarkerDiffCallback(List oldList, List newList) { 164 | this.mOldList = oldList; 165 | this.mNewList = newList; 166 | } 167 | 168 | @Override 169 | public int getOldListSize() { 170 | return mOldList != null ? mOldList.size() : 0; 171 | } 172 | 173 | @Override 174 | public int getNewListSize() { 175 | return mNewList != null ? mNewList.size() : 0; 176 | } 177 | 178 | @Override 179 | public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 180 | return mNewList.get(newItemPosition).getLatLng().equals(mOldList.get(oldItemPosition).getLatLng()); 181 | } 182 | 183 | @Override 184 | public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 185 | return mNewList.get(newItemPosition).equals(mOldList.get(oldItemPosition)); 186 | } 187 | 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/activities/ChoiceActivity.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample.activities; 2 | 3 | import android.os.Bundle; 4 | import androidx.appcompat.app.AppCompatActivity; 5 | import android.view.View; 6 | import android.widget.ImageView; 7 | 8 | import com.mapbox.mapboxsdk.Mapbox; 9 | import com.mapbox.services.Constants; 10 | import com.mapbox.services.api.ServicesException; 11 | import com.mapbox.services.api.staticimage.v1.MapboxStaticImage; 12 | import com.squareup.picasso.Picasso; 13 | 14 | import nz.co.trademe.mapme.sample.R; 15 | 16 | public class ChoiceActivity extends AppCompatActivity { 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.choice_activity); 22 | 23 | View googleMapsButton = findViewById(R.id.google_maps_container); 24 | googleMapsButton.setOnClickListener(new View.OnClickListener() { 25 | @Override 26 | public void onClick(View v) { 27 | GoogleMapsActivity.start(ChoiceActivity.this); 28 | } 29 | }); 30 | 31 | View mapBoxButton = findViewById(R.id.mapbox_container); 32 | mapBoxButton.setOnClickListener(new View.OnClickListener() { 33 | @Override 34 | public void onClick(View v) { 35 | MapBoxActivity.start(ChoiceActivity.this); 36 | } 37 | }); 38 | 39 | ImageView mapboxImageView = (ImageView) findViewById(R.id.mapbox_imageview); 40 | 41 | try { 42 | MapboxStaticImage staticImage = new MapboxStaticImage.Builder() 43 | .setAccessToken(Mapbox.getAccessToken()) 44 | .setUsername(Constants.MAPBOX_USER) 45 | .setStyleId("light-v9") 46 | .setLat(nz.co.trademe.mapme.sample.Constants.AUCKLAND_LAT) 47 | .setLon(nz.co.trademe.mapme.sample.Constants.AUCKLAND_LON) 48 | .setZoom(13) 49 | .setWidth(1280) 50 | .setHeight(600) 51 | .setRetina(true) // Retina 2x image will be returned 52 | .build(); 53 | 54 | Picasso.with(this) 55 | .load(staticImage.getUrl().url().toString()).into(mapboxImageView); 56 | } catch (ServicesException e) { 57 | e.printStackTrace(); 58 | } 59 | 60 | getSupportFragmentManager().findFragmentById(R.id.google_lite_fragment).getView().setClickable(false); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/activities/GoogleMapsActivity.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample.activities; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import androidx.annotation.NonNull; 7 | import android.view.View; 8 | 9 | import com.google.android.gms.maps.CameraUpdateFactory; 10 | import com.google.android.gms.maps.GoogleMap; 11 | import com.google.android.gms.maps.OnMapReadyCallback; 12 | import com.google.android.gms.maps.SupportMapFragment; 13 | import com.google.android.gms.maps.model.Marker; 14 | 15 | import nz.co.trademe.mapme.googlemaps.GoogleMapAnnotationFactory; 16 | import nz.co.trademe.mapme.googlemaps.GoogleMapUtils; 17 | import nz.co.trademe.mapme.sample.R; 18 | 19 | public class GoogleMapsActivity extends MapActivity 20 | implements OnMapReadyCallback, GoogleMap.OnMarkerClickListener { 21 | 22 | public static void start(@NonNull Context context) { 23 | Intent intent = new Intent(context, GoogleMapsActivity.class); 24 | context.startActivity(intent); 25 | } 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.googlemaps_activity); 31 | 32 | SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() 33 | .findFragmentById(R.id.map); 34 | mapFragment.getMapAsync(this); 35 | } 36 | 37 | @Override 38 | View getMapView() { 39 | return ((SupportMapFragment)getSupportFragmentManager().findFragmentById(R.id.map)).getView(); 40 | } 41 | 42 | @Override 43 | public void onMapReady(GoogleMap map) { 44 | onMapReady(new GoogleMapAnnotationFactory(), map); 45 | map.moveCamera(CameraUpdateFactory.newLatLngZoom(GoogleMapUtils.toGoogleMapsLatLng(aucklandLatLng), 14)); 46 | } 47 | 48 | @Override 49 | public boolean onMarkerClick(Marker marker) { 50 | mapsAdapter.notifyAnnotatedMarkerClicked(marker); 51 | return true; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/activities/MapActivity.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample.activities; 2 | 3 | import android.os.Bundle; 4 | import androidx.annotation.Nullable; 5 | import androidx.recyclerview.widget.DiffUtil; 6 | import android.view.View; 7 | 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.util.List; 11 | 12 | import nz.co.trademe.mapme.annotations.AnnotationFactory; 13 | import nz.co.trademe.mapme.annotations.MapAnnotation; 14 | import nz.co.trademe.mapme.annotations.OnMapAnnotationClickListener; 15 | import nz.co.trademe.mapme.sample.MarkerBottomSheet; 16 | import nz.co.trademe.mapme.sample.MarkerData; 17 | import nz.co.trademe.mapme.sample.SampleMapMeAdapter; 18 | import timber.log.Timber; 19 | 20 | public abstract class MapActivity extends BaseActivity implements OnMapAnnotationClickListener { 21 | 22 | protected SampleMapMeAdapter mapsAdapter; 23 | 24 | @Override 25 | protected void onCreate(@Nullable Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | 28 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 29 | } 30 | 31 | public void onMapReady(AnnotationFactory annotationFactory, M map) { 32 | mapsAdapter = new SampleMapMeAdapter<>(this, this.markers, annotationFactory); 33 | mapsAdapter.attach(getMapView(), map); 34 | mapsAdapter.setOnAnnotationClickListener(this); 35 | 36 | List newMarkers = addSampleMarkers(this.markers, MARKER_COUNT); 37 | onMarkersChanged(newMarkers); 38 | } 39 | 40 | @Override 41 | void dispatchDiffUtilResult(DiffUtil.DiffResult result) { 42 | result.dispatchUpdatesTo(mapsAdapter); 43 | } 44 | 45 | abstract View getMapView(); 46 | 47 | @Override 48 | public boolean onMapAnnotationClick(@NotNull MapAnnotation mapAnnotation) { 49 | Timber.d("annotation click: " + mapAnnotation); 50 | 51 | MarkerBottomSheet bottomSheetDialog = MarkerBottomSheet.newInstance(mapAnnotation); 52 | bottomSheetDialog.show(getSupportFragmentManager(), "bottomSheet"); 53 | 54 | MarkerData data = markers.get(mapAnnotation.getPosition()); 55 | data.setSelected(true); 56 | mapsAdapter.notifyItemChanged(mapAnnotation.getPosition()); 57 | return true; 58 | } 59 | 60 | public void removeAnnotation(MapAnnotation mapAnnotation) { 61 | markers.remove(mapAnnotation.getPosition()); 62 | mapsAdapter.notifyItemRemoved(mapAnnotation.getPosition()); 63 | } 64 | 65 | public void unselectMarker(MapAnnotation mapAnnotation) { 66 | markers.get(mapAnnotation.getPosition()).setSelected(false); 67 | mapsAdapter.notifyItemChanged(mapAnnotation.getPosition()); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /sample/src/main/java/nz/co/trademe/mapme/sample/activities/MapBoxActivity.java: -------------------------------------------------------------------------------- 1 | package nz.co.trademe.mapme.sample.activities; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import androidx.annotation.NonNull; 7 | import android.view.View; 8 | 9 | import com.mapbox.mapboxsdk.annotations.Marker; 10 | import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; 11 | import com.mapbox.mapboxsdk.constants.Style; 12 | import com.mapbox.mapboxsdk.maps.MapView; 13 | import com.mapbox.mapboxsdk.maps.MapboxMap; 14 | import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; 15 | 16 | import nz.co.trademe.mapme.mapbox.MapBoxUtils; 17 | import nz.co.trademe.mapme.mapbox.MapboxAnnotationFactory; 18 | import nz.co.trademe.mapme.sample.R; 19 | 20 | public class MapBoxActivity extends MapActivity 21 | implements OnMapReadyCallback, MapboxMap.OnMarkerClickListener { 22 | 23 | private MapView mapView; 24 | 25 | public static void start(@NonNull Context context) { 26 | Intent intent = new Intent(context, MapBoxActivity.class); 27 | context.startActivity(intent); 28 | } 29 | 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.mapbox_activity); 34 | 35 | mapView = (MapView) findViewById(R.id.mapview); 36 | mapView.onCreate(savedInstanceState); 37 | mapView.setStyleUrl(Style.LIGHT); 38 | 39 | mapView.getMapAsync(this); 40 | } 41 | 42 | @Override 43 | protected void onResume() { 44 | super.onResume(); 45 | mapView.onResume(); 46 | } 47 | 48 | @Override 49 | protected void onPause() { 50 | super.onPause(); 51 | mapView.onPause(); 52 | } 53 | 54 | @Override 55 | protected void onSaveInstanceState(Bundle outState) { 56 | super.onSaveInstanceState(outState); 57 | mapView.onSaveInstanceState(outState); 58 | } 59 | 60 | @Override 61 | public void onLowMemory() { 62 | super.onLowMemory(); 63 | mapView.onLowMemory(); 64 | } 65 | 66 | @Override 67 | protected void onDestroy() { 68 | super.onDestroy(); 69 | mapView.onDestroy(); 70 | } 71 | 72 | @Override 73 | public void onMapReady(MapboxMap map) { 74 | onMapReady(new MapboxAnnotationFactory(), map); 75 | map.moveCamera(CameraUpdateFactory.newLatLngZoom(MapBoxUtils.toMapBoxLatLng(aucklandLatLng), 13)); 76 | } 77 | 78 | @Override 79 | View getMapView() { 80 | return mapView; 81 | } 82 | 83 | @Override 84 | public boolean onMarkerClick(@NonNull Marker marker) { 85 | mapsAdapter.notifyAnnotatedMarkerClicked(marker); 86 | return true; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/marker_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-hdpi/marker_blue.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/marker_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-hdpi/marker_green.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/marker_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-hdpi/marker_red.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/marker_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-mdpi/marker_blue.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/marker_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-mdpi/marker_green.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/marker_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-mdpi/marker_red.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/marker_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-xhdpi/marker_blue.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/marker_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-xhdpi/marker_green.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/marker_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-xhdpi/marker_red.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/marker_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-xxhdpi/marker_blue.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/marker_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-xxhdpi/marker_green.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/marker_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-xxhdpi/marker_red.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/marker_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-xxxhdpi/marker_blue.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/marker_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-xxxhdpi/marker_green.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/marker_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/drawable-xxxhdpi/marker_red.png -------------------------------------------------------------------------------- /sample/src/main/res/layout/choice_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 20 | 21 | 38 | 39 | 47 | 48 | 49 | 50 | 57 | 58 | 65 | 66 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/googlemaps_activity.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/mapbox_activity.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/marker_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/res/menu/menu.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 5 | 10 | 11 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TradeMe/MapMe/df0e928452c9d9fa001fb51f6af103b509830047/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | 8 | #ef5350 9 | #42A5F5 10 | #66BB6A 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 4 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MapMe 3 | Google Maps 4 | MapBox 5 | Map type 6 | Remove 7 | Add 10 markers 8 | Remove 5 9 | Add 5, remove 5 10 | Change 5 11 | Move and change 12 | Move and change special 13 | Detect moves 14 | RecyclerView 15 | MapMe - Google Maps 16 | MapMe - Mapbox 17 | 18 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 | 15 | 16 | 19 | 20 |