diff --git a/ApiDemos/kotlin/app/build.gradle b/ApiDemos/kotlin/app/build.gradle index d7aba0ca..f4ea3374 100644 --- a/ApiDemos/kotlin/app/build.gradle +++ b/ApiDemos/kotlin/app/build.gradle @@ -27,8 +27,16 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation 'com.android.support:appcompat-v7:27.0.2' implementation 'com.android.support.constraint:constraint-layout:1.0.2' + // This dependency is needed to use RecyclerView, for the LiteListDemoActivity + implementation "com.android.support:recyclerview-v7:27.0.2" + // CardView is used in the LiteListDemoActivity + implementation 'com.android.support:cardview-v7:27.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + implementation 'com.google.android.gms:play-services-maps:11.8.0' + compile 'com.google.android.gms:play-services-location:11.8.0' + // EasyPermissions is needed to help us request for permission to access location + implementation 'pub.devrel:easypermissions:1.1.1' } \ No newline at end of file diff --git a/ApiDemos/kotlin/app/src/debug/res/values/google_maps_api.xml b/ApiDemos/kotlin/app/src/debug/res/values/google_maps_api.xml new file mode 100644 index 00000000..e2924af4 --- /dev/null +++ b/ApiDemos/kotlin/app/src/debug/res/values/google_maps_api.xml @@ -0,0 +1,17 @@ + + + + ADD_YOUR_KEY_HERE + + diff --git a/ApiDemos/kotlin/app/src/main/AndroidManifest.xml b/ApiDemos/kotlin/app/src/main/AndroidManifest.xml index 2d16c4c9..5ee57a1b 100644 --- a/ApiDemos/kotlin/app/src/main/AndroidManifest.xml +++ b/ApiDemos/kotlin/app/src/main/AndroidManifest.xml @@ -16,6 +16,9 @@ + + + + + + @@ -30,6 +38,12 @@ + + + + + + \ No newline at end of file diff --git a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/BasicMapDemoActivity.kt b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/BasicMapDemoActivity.kt new file mode 100644 index 00000000..fda018cf --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/BasicMapDemoActivity.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.kotlindemos + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle + +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.SupportMapFragment +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.MarkerOptions + +/** + * This shows how to create a simple activity with a map and a marker on the map. + */ +class BasicMapDemoActivity : + AppCompatActivity(), + OnMapReadyCallback { + + val SYDNEY = LatLng(-33.862, 151.21) + val ZOOM_LEVEL = 13f + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_basic_map_demo) + val mapFragment : SupportMapFragment? = + supportFragmentManager.findFragmentById(R.id.map) as? SupportMapFragment + mapFragment?.getMapAsync(this) + } + + /** + * This is where we can add markers or lines, add listeners or move the camera. In this case, + * we just move the camera to Sydney and add a marker in Sydney. + */ + override fun onMapReady(googleMap: GoogleMap) { + with(googleMap) { + moveCamera(CameraUpdateFactory.newLatLngZoom(SYDNEY, ZOOM_LEVEL)) + addMarker(MarkerOptions().position(SYDNEY)) + } + } +} diff --git a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/CircleDemoActivity.kt b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/CircleDemoActivity.kt new file mode 100644 index 00000000..d89851e8 --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/CircleDemoActivity.kt @@ -0,0 +1,322 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.kotlindemos + +import android.graphics.Color +import android.graphics.Point +import android.location.Location +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.CheckBox +import android.widget.SeekBar +import android.widget.Spinner + +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.SupportMapFragment +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.Circle +import com.google.android.gms.maps.model.CircleOptions +import com.google.android.gms.maps.model.Dash +import com.google.android.gms.maps.model.Dot +import com.google.android.gms.maps.model.Gap +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.Marker +import com.google.android.gms.maps.model.MarkerOptions +import com.google.android.gms.maps.model.PatternItem + +import java.util.ArrayList +import java.util.Arrays + +/** + * This shows how to draw circles on a map. + */ +class CircleDemoActivity : + AppCompatActivity(), + SeekBar.OnSeekBarChangeListener, + AdapterView.OnItemSelectedListener, + OnMapReadyCallback { + + private val DEFAULT_RADIUS_METERS = 1000000.0 + + private val MAX_WIDTH_PX = 50 + private val MAX_HUE_DEGREE = 360 + + private val MAX_ALPHA = 255 + private val PATTERN_DASH_LENGTH = 100 + private val PATTERN_GAP_LENGTH = 200 + + private val sydney = LatLng(-33.87365, 151.20689) + + private val dot = Dot() + private val dash = Dash(PATTERN_DASH_LENGTH.toFloat()) + private val gap = Gap(PATTERN_GAP_LENGTH.toFloat()) + private val patternDotted = Arrays.asList(dot, gap) + private val patternDashed = Arrays.asList(dash, gap) + private val patternMixed = Arrays.asList(dot, gap, dot, dash, gap) + + // These are the options for stroke patterns + private val patterns: List?>> = listOf( + Pair(R.string.pattern_solid, null), + Pair(R.string.pattern_dashed, patternDashed), + Pair(R.string.pattern_dotted, patternDotted), + Pair(R.string.pattern_mixed, patternMixed) + ) + + private lateinit var map: GoogleMap + + private val circles = ArrayList(1) + + private var fillColorArgb : Int = 0 + private var strokeColorArgb: Int = 0 + + private lateinit var fillHueBar: SeekBar + private lateinit var fillAlphaBar: SeekBar + private lateinit var strokeWidthBar: SeekBar + private lateinit var strokeHueBar: SeekBar + private lateinit var strokeAlphaBar: SeekBar + private lateinit var strokePatternSpinner: Spinner + private lateinit var clickabilityCheckbox: CheckBox + + /** + * This class contains information about a circle, including its markers + */ + private inner class DraggableCircle(center: LatLng, private var radiusMeters: Double) { + private val centerMarker: Marker = map.addMarker(MarkerOptions().apply { + position(center) + draggable(true) + }) + + private val radiusMarker: Marker = map.addMarker( + MarkerOptions().apply { + position(center.getPointAtDistance(radiusMeters)) + icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE)) + draggable(true) + }) + + private val circle: Circle = map.addCircle( + CircleOptions().apply { + center(center) + radius(radiusMeters) + strokeWidth(strokeWidthBar.progress.toFloat()) + strokeColor(strokeColorArgb) + fillColor(fillColorArgb) + clickable(clickabilityCheckbox.isChecked) + strokePattern(getSelectedPattern(strokePatternSpinner.selectedItemPosition)) + }) + + fun onMarkerMoved(marker: Marker): Boolean { + when (marker) { + centerMarker -> { + circle.center = marker.position + radiusMarker.position = marker.position.getPointAtDistance(radiusMeters) + } + radiusMarker -> { + radiusMeters = centerMarker.position.distanceFrom(radiusMarker.position) + circle.radius = radiusMeters + } + else -> return false + } + return true + } + + fun onStyleChange() { + with(circle) { + strokeWidth = strokeWidthBar.progress.toFloat() + strokeColor = strokeColorArgb + fillColor = fillColorArgb + } + } + + fun setStrokePattern(pattern: List?) { + circle.strokePattern = pattern + } + + fun setClickable(clickable: Boolean) { + circle.isClickable = clickable + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_circle_demo) + + // Set all the SeekBars + fillHueBar = findViewById(R.id.fillHueSeekBar).apply { + max = MAX_HUE_DEGREE + progress = MAX_HUE_DEGREE / 2 + } + fillAlphaBar = findViewById(R.id.fillAlphaSeekBar).apply { + max = MAX_ALPHA + progress = MAX_ALPHA / 2 + } + strokeWidthBar = findViewById(R.id.strokeWidthSeekBar).apply { + max = MAX_WIDTH_PX + progress = MAX_WIDTH_PX / 3 + } + strokeHueBar = findViewById(R.id.strokeHueSeekBar).apply { + max = MAX_HUE_DEGREE + progress = 0 + } + strokeAlphaBar = findViewById(R.id.strokeAlphaSeekBar).apply { + max = MAX_ALPHA + progress = MAX_ALPHA + } + + strokePatternSpinner = findViewById(R.id.strokePatternSpinner).apply { + adapter = ArrayAdapter(this@CircleDemoActivity, + android.R.layout.simple_spinner_item, + getResourceStrings()) + } + + clickabilityCheckbox = findViewById(R.id.toggleClickability) + + val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment + mapFragment.getMapAsync(this) + } + + /** Get all the strings of patterns and return them as Array. */ + private fun getResourceStrings() = (patterns).map { getString(it.first) }.toTypedArray() + + /** + * When the map is ready, move the camera to put the Circle in the middle of the screen, + * create a circle in Sydney, and set the listeners for the map, circles, and SeekBars. + */ + override fun onMapReady(googleMap: GoogleMap?) { + map = googleMap ?: return + // we need to initialise map before creating a circle + with(map) { + moveCamera(CameraUpdateFactory.newLatLngZoom(sydney, 4.0f)) + setContentDescription(getString(R.string.circle_demo_details)) + setOnMapLongClickListener { point -> + // We know the center, let's place the outline at a point 3/4 along the view. + val view: View = supportFragmentManager.findFragmentById(R.id.map).view as View + val radiusLatLng = map.projection.fromScreenLocation( + Point(view.height * 3 / 4, view.width * 3 / 4)) + // Create the circle. + val newCircle = DraggableCircle(point, point.distanceFrom(radiusLatLng)) + circles.add(newCircle) + } + + setOnMarkerDragListener(object : GoogleMap.OnMarkerDragListener { + override fun onMarkerDragStart(marker: Marker) { + onMarkerMoved(marker) + } + + override fun onMarkerDragEnd(marker: Marker) { + onMarkerMoved(marker) + } + + override fun onMarkerDrag(marker: Marker) { + onMarkerMoved(marker) + } + }) + + // Flip the red, green and blue components of the circle's stroke color. + setOnCircleClickListener { c -> c.strokeColor = c.strokeColor xor 0x00ffffff } + } + + fillColorArgb = Color.HSVToColor(fillAlphaBar.progress, + floatArrayOf(fillHueBar.progress.toFloat(), 1f, 1f)) + strokeColorArgb = Color.HSVToColor(strokeAlphaBar.progress, + floatArrayOf(strokeHueBar.progress.toFloat(), 1f, 1f)) + + val circle = DraggableCircle(sydney, DEFAULT_RADIUS_METERS) + circles.add(circle) + + // Set listeners for all the SeekBar + fillHueBar.setOnSeekBarChangeListener(this) + fillAlphaBar.setOnSeekBarChangeListener(this) + + strokeWidthBar.setOnSeekBarChangeListener(this) + strokeHueBar.setOnSeekBarChangeListener(this) + strokeAlphaBar.setOnSeekBarChangeListener(this) + + strokePatternSpinner.onItemSelectedListener = this + } + + private fun getSelectedPattern(pos: Int): List? = patterns[pos].second + + override fun onItemSelected(parent: AdapterView<*>, view: View, pos: Int, id: Long) { + if (parent.id == R.id.strokePatternSpinner) { + circles.map { it.setStrokePattern(getSelectedPattern(pos)) } + } + } + + override fun onNothingSelected(parent: AdapterView<*>) { + // Don't do anything here. + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + // Don't do anything here. + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + // Don't do anything here. + } + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + // Update the fillColorArgb if the SeekBars for it is changed, otherwise keep the old value + fillColorArgb = when (seekBar) { + fillHueBar -> Color.HSVToColor(Color.alpha(fillColorArgb), + floatArrayOf(progress.toFloat(), 1f, 1f)) + fillAlphaBar -> Color.argb(progress, Color.red(fillColorArgb), + Color.green(fillColorArgb), Color.blue(fillColorArgb)) + else -> fillColorArgb + } + + // Set the strokeColorArgb if the SeekBars for it is changed, otherwise keep the old value + strokeColorArgb = when (seekBar) { + strokeHueBar -> Color.HSVToColor(Color.alpha(strokeColorArgb), + floatArrayOf(progress.toFloat(), 1f, 1f)) + strokeAlphaBar -> Color.argb(progress, Color.red(strokeColorArgb), + Color.green(strokeColorArgb), Color.blue(strokeColorArgb)) + else -> strokeColorArgb + } + + circles.map { it.onStyleChange() } + } + + private fun onMarkerMoved(marker: Marker) { + circles.forEach { if (it.onMarkerMoved(marker)) return } + } + + /** Listener for the Clickable CheckBox, to set if all the circles can be click */ + fun toggleClickability(view: View) { + circles.map { it.setClickable((view as CheckBox).isChecked) } + } +} + +/** + * Extension function to find the distance from this to another LatLng object + */ +private fun LatLng.distanceFrom(other: LatLng): Double { + val result = FloatArray(1) + Location.distanceBetween(latitude, longitude, other.latitude, other.longitude, result) + return result[0].toDouble() +} + +private fun LatLng.getPointAtDistance(distance: Double): LatLng { + val radiusOfEarth = 6371009.0 + val radiusAngle = (Math.toDegrees(distance / radiusOfEarth) + / Math.cos(Math.toRadians(latitude))) + return LatLng(latitude, longitude + radiusAngle) +} \ No newline at end of file diff --git a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/CloseInfoWindowDemoActivity.kt b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/CloseInfoWindowDemoActivity.kt new file mode 100644 index 00000000..f957d110 --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/CloseInfoWindowDemoActivity.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.kotlindemos + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import com.example.kotlindemos.OnMapAndViewReadyListener.OnGlobalLayoutAndMapReadyListener +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.SupportMapFragment +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.google.android.gms.maps.model.Marker +import com.google.android.gms.maps.model.MarkerOptions + + +/** + * This shows how to close the info window when the currently selected marker is re-tapped. + */ +class CloseInfoWindowDemoActivity : + AppCompatActivity(), + OnGlobalLayoutAndMapReadyListener { + + private lateinit var map: GoogleMap + + /** Keeps track of the selected marker. It will be set to null if no marker is selected. */ + private var selectedMarker: Marker? = null + + /** + * If user tapped on the the marker which was already showing info window, + * the showing info window will be closed. Otherwise will show a different window. + */ + private val markerClickListener = object : GoogleMap.OnMarkerClickListener { + override fun onMarkerClick(marker: Marker?): Boolean { + if (marker == selectedMarker) { + selectedMarker = null + // Return true to indicate we have consumed the event and that we do not + // want the the default behavior to occur (which is for the camera to move + // such that the marker is centered and for the marker's info window to open, + // if it has one). + return true + } + + selectedMarker = marker + // Return false to indicate that we have not consumed the event and that + // we wish for the default behavior to occur. + return false + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_marker_close_info_window_on_retap_demo) + + val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment + OnMapAndViewReadyListener(mapFragment, this) + } + + override fun onMapReady(googleMap: GoogleMap?) { + // Return if googleMap was null + map = googleMap ?: return + + with(map) { + uiSettings.isZoomControlsEnabled = false + + setOnMarkerClickListener(markerClickListener) + + // Set listener for map click event. Any showing info window closes + // when the map is clicked. Clear the currently selected marker. + setOnMapClickListener { selectedMarker = null } + + setContentDescription(getString(R.string.close_info_window_demo_details)) + + // Add markers to different cities in Australia and include it in bounds + val bounds = LatLngBounds.Builder() + cities.map { city -> + addMarker(MarkerOptions().apply { + position(city.latLng) + title(city.title) + snippet(city.snippet) + }) + bounds.include(city.latLng) + } + + moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 50)) + } + } + + /** + * Class to contain information about a marker. + * + * @property latLng latitude and longitude of the marker + * @property title a string containing the city name + * @property snippet a string containing the population of the city + */ + class MarkerInfo(val latLng: LatLng, val title: String, val snippet: String) + + private val cities = listOf( + MarkerInfo(LatLng(-27.47093, 153.0235), + "Brisbane", "Population: 2,074,200"), + MarkerInfo(LatLng(-37.81319, 144.96298), + "Melbourne", "Population: 4,137,400"), + MarkerInfo(LatLng(-33.87365, 151.20689), + "Sydney", "Population: 4,627,300"), + MarkerInfo(LatLng(-34.92873, 138.59995), + "Adelaide", "Population: 1,213,000"), + MarkerInfo(LatLng(-31.952854, 115.857342), + "Perth", "Population: 1,738,800") + ) +} diff --git a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/DemoDetailsList.kt b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/DemoDetailsList.kt index 901f7f4e..225a27ed 100644 --- a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/DemoDetailsList.kt +++ b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/DemoDetailsList.kt @@ -21,6 +21,22 @@ package com.example.kotlindemos */ class DemoDetailsList { companion object { - val DEMOS = listOf() + val DEMOS = listOf( + DemoDetails(R.string.basic_demo_label, R.string.basic_demo_details, + BasicMapDemoActivity::class.java), + DemoDetails(R.string.close_info_window_demo_label, + R.string.close_info_window_demo_details, + CloseInfoWindowDemoActivity::class.java), + DemoDetails(R.string.circle_demo_label, R.string.circle_demo_details, + CircleDemoActivity::class.java), + DemoDetails(R.string.lite_list_demo_label, R.string.lite_list_demo_details, + LiteListDemoActivity::class.java), + DemoDetails( + R.string.street_view_panorama_navigation_demo_label, + R.string.street_view_panorama_navigation_demo_details, + StreetViewPanoramaNavigationDemoActivity::class.java), + DemoDetails(R.string.ui_settings_demo_label, R.string.ui_settings_demo_details, + UiSettingsDemoActivity::class.java) + ) } } \ No newline at end of file diff --git a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/LiteListDemoActivity.kt b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/LiteListDemoActivity.kt new file mode 100644 index 00000000..a9f02e49 --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/LiteListDemoActivity.kt @@ -0,0 +1,218 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.kotlindemos + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.MapsInitializer +import com.google.android.gms.maps.MapView +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.MarkerOptions + +/** + * This shows to include a map in lite mode in a RecyclerView. + * Note the use of the view holder pattern with the + * [com.google.android.gms.maps.OnMapReadyCallback]. + */ +class LiteListDemoActivity : AppCompatActivity() { + + private val linearLayoutManager: LinearLayoutManager by lazy { + LinearLayoutManager(this) + } + + private val gridLayoutManager: GridLayoutManager by lazy { + GridLayoutManager(this, 2) + } + + private lateinit var recyclerView: RecyclerView + private lateinit var mapAdapter: RecyclerView.Adapter + + /** + * RecycleListener that completely clears the [com.google.android.gms.maps.GoogleMap] + * attached to a row in the RecyclerView. + * Sets the map type to [com.google.android.gms.maps.GoogleMap.MAP_TYPE_NONE] and clears + * the map. + */ + private val recycleListener = RecyclerView.RecyclerListener { holder -> + val mapHolder = holder as MapAdapter.ViewHolder + mapHolder.clearView() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_lite_list_demo) + + mapAdapter = MapAdapter() + + // Initialise the RecyclerView. + recyclerView = findViewById(R.id.recycler_view).apply { + setHasFixedSize(true) + layoutManager = linearLayoutManager + adapter = mapAdapter + setRecyclerListener(recycleListener) + } + } + + /** Create options menu to switch between the linear and grid layout managers. */ + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.lite_list_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + recyclerView.layoutManager = when (item?.itemId) { + R.id.layout_linear -> linearLayoutManager + R.id.layout_grid -> gridLayoutManager + else -> return false + } + return true + } + + /** + * Adapter that displays a title and [com.google.android.gms.maps.MapView] for each item. + * The layout is defined in `lite_list_demo_row.xml`. It contains a MapView + * that is programatically initialised when onCreateViewHolder is called. + */ + inner class MapAdapter : RecyclerView.Adapter() { + + override fun onBindViewHolder(holder: ViewHolder?, position: Int) { + holder?.bindView(position) ?: return + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflated = LayoutInflater.from(parent.context) + .inflate(R.layout.lite_list_demo_row, parent, false) + return ViewHolder(inflated) + } + + override fun getItemCount() = listLocations.size + + /** A view holder for the map and title. */ + inner class ViewHolder(view: View) : + RecyclerView.ViewHolder(view), + OnMapReadyCallback { + + private val layout: View = view + private val mapView: MapView = layout.findViewById(R.id.lite_listrow_map) + private val title: TextView = layout.findViewById(R.id.lite_listrow_text) + private lateinit var map: GoogleMap + private lateinit var latLng: LatLng + + /** Initialises the MapView by calling its lifecycle methods */ + init { + with(mapView) { + // Initialise the MapView + onCreate(null) + // Set the map ready callback to receive the GoogleMap object + getMapAsync(this@ViewHolder) + } + } + + private fun setMapLocation() { + if (!::map.isInitialized) return + with(map) { + moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 13f)) + addMarker(MarkerOptions().position(latLng)) + mapType = GoogleMap.MAP_TYPE_NORMAL + setOnMapClickListener { + Toast.makeText(this@LiteListDemoActivity, "Clicked on ${title.text}", + Toast.LENGTH_SHORT).show() + } + } + } + + override fun onMapReady(googleMap: GoogleMap?) { + MapsInitializer.initialize(applicationContext) + // If map is not initialised properly + map = googleMap ?: return + setMapLocation() + } + + /** This function is called when the RecyclerView wants to bind the ViewHolder. */ + fun bindView(position: Int) { + listLocations[position].let { + latLng = it.second + mapView.tag = this + title.text = it.first + // We need to call setMapLocation from here because RecyclerView might use the + // previously loaded maps + setMapLocation() + } + } + + /** This function is called by the recycleListener, when we need to clear the map. */ + fun clearView() { + with(map) { + // Clear the map and free up resources by changing the map type to none + clear() + mapType = GoogleMap.MAP_TYPE_NONE + } + } + } + } + + /** A list of locations of to show in the RecyclerView */ + private val listLocations: List> = listOf( + Pair("Cape Town", LatLng(-33.920455, 18.466941)), + Pair("Beijing", LatLng(39.937795, 116.387224)), + Pair("Bern", LatLng(46.948020, 7.448206)), + Pair("Breda", LatLng(51.589256, 4.774396)), + Pair("Brussels", LatLng(50.854509, 4.376678)), + Pair("Copenhagen", LatLng(55.679423, 12.577114)), + Pair("Hannover", LatLng(52.372026, 9.735672)), + Pair("Helsinki", LatLng(60.169653, 24.939480)), + Pair("Hong Kong", LatLng(22.325862, 114.165532)), + Pair("Istanbul", LatLng(41.034435, 28.977556)), + Pair("Johannesburg", LatLng(-26.202886, 28.039753)), + Pair("Lisbon", LatLng(38.707163, -9.135517)), + Pair("London", LatLng(51.500208, -0.126729)), + Pair("Madrid", LatLng(40.420006, -3.709924)), + Pair("Mexico City", LatLng(19.427050, -99.127571)), + Pair("Moscow", LatLng(55.750449, 37.621136)), + Pair("New York", LatLng(40.750580, -73.993584)), + Pair("Oslo", LatLng(59.910761, 10.749092)), + Pair("Paris", LatLng(48.859972, 2.340260)), + Pair("Prague", LatLng(50.087811, 14.420460)), + Pair("Rio de Janeiro", LatLng(-22.90187, -43.232437)), + Pair("Rome", LatLng(41.889998, 12.500162)), + Pair("Sao Paolo", LatLng(-22.863878, -43.244097)), + Pair("Seoul", LatLng(37.560908, 126.987705)), + Pair("Stockholm", LatLng(59.330650, 18.067360)), + Pair("Sydney", LatLng(-33.873651, 151.2068896)), + Pair("Taipei", LatLng(25.022112, 121.478019)), + Pair("Tokyo", LatLng(35.670267, 139.769955)), + Pair("Tulsa Oklahoma", LatLng(36.149777, -95.993398)), + Pair("Vaduz", LatLng(47.141076, 9.521482)), + Pair("Vienna", LatLng(48.209206, 16.372778)), + Pair("Warsaw", LatLng(52.235474, 21.004057)), + Pair("Wellington", LatLng(-41.286480, 174.776217)), + Pair("Winnipeg", LatLng(49.875832, -97.150726)) + ) +} diff --git a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MainActivity.kt b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MainActivity.kt index 4ead271d..ad93c28a 100644 --- a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MainActivity.kt +++ b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MainActivity.kt @@ -61,8 +61,8 @@ class MainActivity : AppCompatActivity(), AdapterView.OnItemClickListener { class CustomArrayAdapter(context: Context, demos: List) : ArrayAdapter(context, R.id.title, demos) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup) : View { - val demo : DemoDetails = getItem(position) + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val demo: DemoDetails = getItem(position) return (convertView as? FeatureView ?: FeatureView(context)).apply { setTitleId(demo.titleId) setDescriptionId(demo.descriptionId) diff --git a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/StreetViewPanoramaNavigationDemoActivity.kt b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/StreetViewPanoramaNavigationDemoActivity.kt new file mode 100644 index 00000000..f66e5418 --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/StreetViewPanoramaNavigationDemoActivity.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.kotlindemos + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import android.view.View +import android.widget.SeekBar +import android.widget.Toast +import com.google.android.gms.maps.StreetViewPanorama +import com.google.android.gms.maps.SupportStreetViewPanoramaFragment +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.StreetViewPanoramaCamera +import com.google.android.gms.maps.model.StreetViewPanoramaLink + +/** + * This shows how to create an activity with access to all the options in Panorama + * which can be adjusted dynamically. + */ +class StreetViewPanoramaNavigationDemoActivity : AppCompatActivity() { + + // George St, Sydney + private val sydney = LatLng(-33.87365, 151.20689) + // Cole St, San Francisco + private val sanFrancisco = LatLng(37.769263, -122.450727) + // Santorini, Greece using the Pano ID of the location instead of a LatLng object + private val santoriniPanoId = "WddsUw1geEoAAAQIt9RnsQ" + // LatLng with no panorama + private val invalid = LatLng(-45.125783, 151.276417) + + // The amount in degrees by which to scroll the camera + private val PAN_BY_DEGREES = 30 + // The amount of zoom + private val ZOOM_BY = 0.5f + + private lateinit var streetViewPanorama: StreetViewPanorama + private lateinit var customDurationBar: SeekBar + + private val duration: Long + get() = customDurationBar.progress.toLong() + + override fun onCreate(savedInstanceState:Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_street_view_panorama_navigation_demo) + + val streetViewPanoramaFragment = supportFragmentManager + .findFragmentById(R.id.streetviewpanorama) as SupportStreetViewPanoramaFragment + streetViewPanoramaFragment.getStreetViewPanoramaAsync { panorama -> + streetViewPanorama = panorama + // Only set the panorama to sydney on startup (when no panoramas have been + // loaded which is when the savedInstanceState is null). + if (savedInstanceState == null) { + streetViewPanorama.setPosition(sydney) + } + } + + customDurationBar = findViewById(R.id.duration_bar) + } + + /** + * Checks if the map is ready, the executes the provided lamdba function + * + * @param stuffToDo the code to be executed if [streetViewPanorama] is ready + */ + private fun checkReadyThen(stuffToDo : () -> Unit) { + if (!::streetViewPanorama.isInitialized) { + Toast.makeText(this, R.string.map_not_ready, Toast.LENGTH_SHORT).show() + } else { + stuffToDo() + } + } + + /** + * Called when the Go To Location button is pressed + */ + fun onGoToLocation(view: View) { + when (view.id) { + R.id.sydney -> streetViewPanorama.setPosition(sydney) + R.id.sanfrancisco -> streetViewPanorama.setPosition(sanFrancisco) + R.id.santorini -> streetViewPanorama.setPosition(santoriniPanoId) + R.id.invalid -> streetViewPanorama.setPosition(invalid) + } + } + + /** + * This function is called from the listeners, and builds the street view panorama with the + * specified arguments. + */ + private fun updateStreetViewPanorama(zoom: Float, tilt: Float, bearing: Float) { + streetViewPanorama.animateTo(StreetViewPanoramaCamera.Builder().apply { + zoom(zoom) + tilt(tilt) + bearing(bearing) + } .build(), duration) + } + + fun onButtonClicked(view: View) { + checkReadyThen { + with(streetViewPanorama.panoramaCamera) { + when (view.id) { + R.id.zoom_in -> updateStreetViewPanorama(zoom + ZOOM_BY, tilt, bearing) + R.id.zoom_out -> updateStreetViewPanorama(zoom - ZOOM_BY, tilt, bearing) + R.id.pan_left -> updateStreetViewPanorama(zoom, tilt, bearing - PAN_BY_DEGREES) + R.id.pan_right -> updateStreetViewPanorama(zoom, tilt, bearing + PAN_BY_DEGREES) + R.id.pan_up -> { + var newTilt = tilt + PAN_BY_DEGREES + if (newTilt > 90) newTilt = 90f + updateStreetViewPanorama(zoom, newTilt, bearing) + } + R.id.pan_down -> { + var newTilt = tilt - PAN_BY_DEGREES + if (newTilt < -90) newTilt = -90f + updateStreetViewPanorama(zoom, newTilt, bearing) + } + } + } + } + } + + fun onRequestPosition(view: View) { + checkReadyThen { + streetViewPanorama.location?.let { + Toast.makeText(view.context, streetViewPanorama.location.position.toString(), + Toast.LENGTH_SHORT).show() + } + } + } + + @Suppress("UNUSED_PARAMETER") + fun onMovePosition(view: View) { + val location = streetViewPanorama.location + val camera = streetViewPanorama.panoramaCamera + location?.links?.let { + val link = location.links.findClosestLinkToBearing(camera.bearing) + streetViewPanorama.setPosition(link.panoId) + } + } + + /** Extension function to find the closest link from a point. */ + private fun Array.findClosestLinkToBearing( + bearing: Float + ): StreetViewPanoramaLink { + + // Find the difference between angle a and b as a value between 0 and 180. + val findNormalizedDifference = fun (a: Float, b: Float): Float { + val diff = a - b + val normalizedDiff = diff - (360 * Math.floor((diff / 360.0f).toDouble())).toFloat() + return if ((normalizedDiff < 180.0f)) normalizedDiff else 360.0f - normalizedDiff + } + + var minBearingDiff = 360f + var closestLink = this[0] + for (link in this) { + if (minBearingDiff > findNormalizedDifference(bearing, link.bearing)) { + minBearingDiff = findNormalizedDifference(bearing, link.bearing) + closestLink = link + } + } + return closestLink + } +} diff --git a/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/UiSettingsDemoActivity.kt b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/UiSettingsDemoActivity.kt new file mode 100644 index 00000000..1f272fea --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/UiSettingsDemoActivity.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.kotlindemos + +import android.Manifest +import android.annotation.SuppressLint +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import android.view.View +import android.widget.CheckBox +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.SupportMapFragment +import pub.devrel.easypermissions.AfterPermissionGranted +import pub.devrel.easypermissions.EasyPermissions + +const val REQUEST_CODE_LOCATION = 123 + +class UiSettingsDemoActivity : + AppCompatActivity(), + OnMapReadyCallback { + + private lateinit var map: GoogleMap + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_ui_settings_demo) + val mapFragment: SupportMapFragment = + supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment + mapFragment.getMapAsync(this) + } + + override fun onMapReady(googleMap: GoogleMap?) { + // Return early if map is not initialised properly + map = googleMap ?: return + enableMyLocation() + // Set all the settings of the map to match the current state of the checkboxes + with(map.uiSettings) { + isZoomControlsEnabled = isChecked(R.id.zoom_button) + isCompassEnabled = isChecked(R.id.compass_button) + isMyLocationButtonEnabled = isChecked(R.id.myloc_button) + isIndoorLevelPickerEnabled = isChecked(R.id.level_button) + isMapToolbarEnabled = isChecked(R.id.maptoolbar_button) + isZoomGesturesEnabled = isChecked(R.id.zoomgest_button) + isScrollGesturesEnabled = isChecked(R.id.scrollgest_button) + isTiltGesturesEnabled = isChecked(R.id.tiltgest_button) + isRotateGesturesEnabled = isChecked(R.id.rotategest_button) + } + } + + /** On click listener for checkboxes */ + fun onClick(view: View) { + if (view !is CheckBox) { + return + } + val isChecked: Boolean = view.isChecked + with(map.uiSettings) { + when (view.id) { + R.id.zoom_button -> isZoomControlsEnabled = isChecked + R.id.compass_button -> isCompassEnabled = isChecked + R.id.myloc_button -> isMyLocationButtonEnabled = isChecked + R.id.level_button -> isIndoorLevelPickerEnabled = isChecked + R.id.maptoolbar_button -> isMapToolbarEnabled = isChecked + R.id.zoomgest_button -> isZoomGesturesEnabled = isChecked + R.id.scrollgest_button -> isScrollGesturesEnabled = isChecked + R.id.tiltgest_button -> isTiltGesturesEnabled = isChecked + R.id.rotategest_button -> isRotateGesturesEnabled = isChecked + } + } + } + + /** Returns whether the checkbox with the given id is checked */ + private fun isChecked(id: Int) = findViewById(id)?.isChecked ?: false + + /** Override the onRequestPermissionResult to use EasyPermissions */ + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + /** + * enableMyLocation() will enable the location of the map if the user has given permission + * for the app to access their device location. + * Android Studio requires an explicit check before setting map.isMyLocationEnabled to true + * but we are using EasyPermissions to handle it so we can suppress the "MissingPermission" + * check. + */ + @SuppressLint("MissingPermission") + @AfterPermissionGranted(REQUEST_CODE_LOCATION) + private fun enableMyLocation() { + if (hasLocationPermission()) { + map.isMyLocationEnabled = true + } else { + EasyPermissions.requestPermissions( + this, + getString(R.string.location), + REQUEST_CODE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION + ) + } + } + + private fun hasLocationPermission(): Boolean { + return EasyPermissions.hasPermissions(this, Manifest.permission.ACCESS_FINE_LOCATION) + } +} diff --git a/ApiDemos/kotlin/app/src/main/res/layout/activity_basic_map_demo.xml b/ApiDemos/kotlin/app/src/main/res/layout/activity_basic_map_demo.xml new file mode 100644 index 00000000..17225bd4 --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/res/layout/activity_basic_map_demo.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/ApiDemos/kotlin/app/src/main/res/layout/activity_circle_demo.xml b/ApiDemos/kotlin/app/src/main/res/layout/activity_circle_demo.xml new file mode 100644 index 00000000..6b7c3c91 --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/res/layout/activity_circle_demo.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ApiDemos/kotlin/app/src/main/res/layout/activity_lite_list_demo.xml b/ApiDemos/kotlin/app/src/main/res/layout/activity_lite_list_demo.xml new file mode 100644 index 00000000..949d8ebf --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/res/layout/activity_lite_list_demo.xml @@ -0,0 +1,22 @@ + + + diff --git a/ApiDemos/kotlin/app/src/main/res/layout/activity_marker_close_info_window_on_retap_demo.xml b/ApiDemos/kotlin/app/src/main/res/layout/activity_marker_close_info_window_on_retap_demo.xml new file mode 100644 index 00000000..027d9c8f --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/res/layout/activity_marker_close_info_window_on_retap_demo.xml @@ -0,0 +1,25 @@ + + + + diff --git a/ApiDemos/kotlin/app/src/main/res/layout/activity_street_view_panorama_navigation_demo.xml b/ApiDemos/kotlin/app/src/main/res/layout/activity_street_view_panorama_navigation_demo.xml new file mode 100644 index 00000000..58f7425b --- /dev/null +++ b/ApiDemos/kotlin/app/src/main/res/layout/activity_street_view_panorama_navigation_demo.xml @@ -0,0 +1,191 @@ + + + + + + + + +