Gesture Detector
The gesture detector of MapMetrics Android is encapsulated in the mapmetrics-gestures-android
package.
Gesture Listeners
You can add listeners for move, rotate, scale and shove gestures. For example, adding a move gesture listener with MapMetricsMap.addOnRotateListener
:
kotlin
mapMetricsMap.addOnMoveListener(
object : OnMoveListener {
override fun onMoveBegin(detector: MoveGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_START, "MOVE START")
)
}
override fun onMove(detector: MoveGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_PROGRESS, "MOVE PROGRESS")
)
}
override fun onMoveEnd(detector: MoveGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_END, "MOVE END")
)
recalculateFocalPoint()
}
}
)
Refer to the full example below for examples of listeners for the other gesture types.
Settings
You can access an UISettings
object via MapMetricsMap.uiSettings
. Available settings include:
- Toggle Quick Zoom. You can double tap on the map to use quick zoom. You can toggle this behavior on and off (
UiSettings.isQuickZoomGesturesEnabled
). - Toggle Velocity Animations. By default flicking causes the map to continue panning (while decelerating). You can turn this off with
UiSettings.isScaleVelocityAnimationEnabled
. - Toggle Rotate Enabled. Use
uiSettings.isRotateGesturesEnabled
. - Toggle Zoom Enabled. Use
uiSettings.isZoomGesturesEnabled
.
Full Example Activity
kotlin
/** Test activity showcasing APIs around gestures implementation. */
class GestureDetectorActivity : AppCompatActivity() {
private lateinit var mapView: MapView
private lateinit var mapMetricsMap: MapMetricsMap
private lateinit var recyclerView: RecyclerView
private var gestureAlertsAdapter: GestureAlertsAdapter? = null
private var gesturesManager: AndroidGesturesManager? = null
private var marker: Marker? = null
private var focalPointLatLng: LatLng? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gesture_detector)
mapView = findViewById(R.id.mapView)
mapView.onCreate(savedInstanceState)
mapView.getMapAsync { map: MapMetricsMap ->
mapMetricsMap = map
mapMetricsMap.setStyle(TestStyles.getPredefinedStyleWithFallback("Streets"))
initializeMap()
}
recyclerView = findViewById(R.id.alerts_recycler)
recyclerView.setLayoutManager(LinearLayoutManager(this))
gestureAlertsAdapter = GestureAlertsAdapter()
recyclerView.setAdapter(gestureAlertsAdapter)
}
override fun onResume() {
super.onResume()
mapView.onResume()
}
override fun onPause() {
super.onPause()
gestureAlertsAdapter!!.cancelUpdates()
mapView.onPause()
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onStop() {
super.onStop()
mapView.onStop()
}
override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
private fun initializeMap() {
gesturesManager = mapMetricsMap.gesturesManager
val layoutParams = recyclerView.layoutParams as RelativeLayout.LayoutParams
layoutParams.height = (mapView.height / 1.75).toInt()
layoutParams.width = mapView.width / 3
recyclerView.layoutParams = layoutParams
attachListeners()
fixedFocalPointEnabled(mapMetricsMap.uiSettings.focalPoint != null)
}
fun attachListeners() {
// # --8<-- [start:addOnMoveListener]
mapMetricsMap.addOnMoveListener(
object : OnMoveListener {
override fun onMoveBegin(detector: MoveGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_START, "MOVE START")
)
}
override fun onMove(detector: MoveGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_PROGRESS, "MOVE PROGRESS")
)
}
override fun onMoveEnd(detector: MoveGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_END, "MOVE END")
)
recalculateFocalPoint()
}
}
)
// # --8<-- [end:addOnMoveListener]
mapMetricsMap.addOnRotateListener(
object : OnRotateListener {
override fun onRotateBegin(detector: RotateGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_START, "ROTATE START")
)
}
override fun onRotate(detector: RotateGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_PROGRESS, "ROTATE PROGRESS")
)
recalculateFocalPoint()
}
override fun onRotateEnd(detector: RotateGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_END, "ROTATE END")
)
}
}
)
mapMetricsMap.addOnScaleListener(
object : OnScaleListener {
override fun onScaleBegin(detector: StandardScaleGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_START, "SCALE START")
)
if (focalPointLatLng != null) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(
GestureAlert.TYPE_OTHER,
"INCREASING MOVE THRESHOLD"
)
)
gesturesManager!!.moveGestureDetector.moveThreshold =
ResourceUtils.convertDpToPx(this@GestureDetectorActivity, 175f)
gestureAlertsAdapter!!.addAlert(
GestureAlert(
GestureAlert.TYPE_OTHER,
"MANUALLY INTERRUPTING MOVE"
)
)
gesturesManager!!.moveGestureDetector.interrupt()
}
recalculateFocalPoint()
}
override fun onScale(detector: StandardScaleGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_PROGRESS, "SCALE PROGRESS")
)
}
override fun onScaleEnd(detector: StandardScaleGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_END, "SCALE END")
)
if (focalPointLatLng != null) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(
GestureAlert.TYPE_OTHER,
"REVERTING MOVE THRESHOLD"
)
)
gesturesManager!!.moveGestureDetector.moveThreshold = 0f
}
}
}
)
mapMetricsMap.addOnShoveListener(
object : OnShoveListener {
override fun onShoveBegin(detector: ShoveGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_START, "SHOVE START")
)
}
override fun onShove(detector: ShoveGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_PROGRESS, "SHOVE PROGRESS")
)
}
override fun onShoveEnd(detector: ShoveGestureDetector) {
gestureAlertsAdapter!!.addAlert(
GestureAlert(GestureAlert.TYPE_END, "SHOVE END")
)
}
}
)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_gestures, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val uiSettings = mapMetricsMap.uiSettings
when (item.itemId) {
R.id.menu_gesture_focus_point -> {
fixedFocalPointEnabled(focalPointLatLng == null)
return true
}
R.id.menu_gesture_animation -> {
uiSettings.isScaleVelocityAnimationEnabled =
!uiSettings.isScaleVelocityAnimationEnabled
uiSettings.isRotateVelocityAnimationEnabled =
!uiSettings.isRotateVelocityAnimationEnabled
uiSettings.isFlingVelocityAnimationEnabled =
!uiSettings.isFlingVelocityAnimationEnabled
return true
}
R.id.menu_gesture_rotate -> {
uiSettings.isRotateGesturesEnabled = !uiSettings.isRotateGesturesEnabled
return true
}
R.id.menu_gesture_tilt -> {
uiSettings.isTiltGesturesEnabled = !uiSettings.isTiltGesturesEnabled
return true
}
R.id.menu_gesture_zoom -> {
uiSettings.isZoomGesturesEnabled = !uiSettings.isZoomGesturesEnabled
return true
}
R.id.menu_gesture_scroll -> {
uiSettings.isScrollGesturesEnabled = !uiSettings.isScrollGesturesEnabled
return true
}
R.id.menu_gesture_double_tap -> {
uiSettings.isDoubleTapGesturesEnabled = !uiSettings.isDoubleTapGesturesEnabled
return true
}
R.id.menu_gesture_quick_zoom -> {
uiSettings.isQuickZoomGesturesEnabled = !uiSettings.isQuickZoomGesturesEnabled
return true
}
R.id.menu_gesture_scroll_horizontal -> {
uiSettings.isHorizontalScrollGesturesEnabled =
!uiSettings.isHorizontalScrollGesturesEnabled
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun fixedFocalPointEnabled(enabled: Boolean) {
if (enabled) {
focalPointLatLng = LatLng(51.50325, -0.12968)
marker = mapMetricsMap.addMarker(MarkerOptions().position(focalPointLatLng))
mapMetricsMap.easeCamera(
CameraUpdateFactory.newLatLngZoom(focalPointLatLng!!, 16.0),
object : CancelableCallback {
override fun onCancel() {
recalculateFocalPoint()
}
override fun onFinish() {
recalculateFocalPoint()
}
}
)
} else {
if (marker != null) {
mapMetricsMap.removeMarker(marker!!)
marker = null
}
focalPointLatLng = null
mapMetricsMap.uiSettings.focalPoint = null
}
}
private fun recalculateFocalPoint() {
if (focalPointLatLng != null) {
mapMetricsMap.uiSettings.focalPoint =
mapMetricsMap.projection.toScreenLocation(focalPointLatLng!!)
}
}
private class GestureAlertsAdapter : RecyclerView.Adapter<GestureAlertsAdapter.ViewHolder>() {
private var isUpdating = false
private val updateHandler = Handler(Looper.getMainLooper())
private val alerts: MutableList<GestureAlert> = ArrayList()
class ViewHolder internal constructor(view: View) : RecyclerView.ViewHolder(view) {
var alertMessageTv: TextView
init {
val typeface = FontCache.get("Roboto-Regular.ttf", view.context)
alertMessageTv = view.findViewById(R.id.alert_message)
alertMessageTv.typeface = typeface
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
LayoutInflater.from(parent.context)
.inflate(R.layout.item_gesture_alert, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val alert = alerts[position]
holder.alertMessageTv.text = alert.message
holder.alertMessageTv.setTextColor(
ContextCompat.getColor(holder.alertMessageTv.context, alert.color)
)
}
override fun getItemCount(): Int {
return alerts.size
}
fun addAlert(alert: GestureAlert) {
for (gestureAlert in alerts) {
if (gestureAlert.alertType != GestureAlert.TYPE_PROGRESS) {
break
}
if (alert.alertType == GestureAlert.TYPE_PROGRESS && gestureAlert == alert) {
return
}
}
if (itemCount >= MAX_NUMBER_OF_ALERTS) {
alerts.removeAt(itemCount - 1)
}
alerts.add(0, alert)
if (!isUpdating) {
isUpdating = true
updateHandler.postDelayed(updateRunnable, 250)
}
}
@SuppressLint("NotifyDataSetChanged")
private val updateRunnable = Runnable {
notifyDataSetChanged()
isUpdating = false
}
fun cancelUpdates() {
updateHandler.removeCallbacksAndMessages(null)
}
}
private class GestureAlert(
@field:Type @param:Type
val alertType: Int,
val message: String?
) {
@Retention(AnnotationRetention.SOURCE)
@IntDef(TYPE_NONE, TYPE_START, TYPE_PROGRESS, TYPE_END, TYPE_OTHER)
annotation class Type
@ColorInt var color = 0
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other == null || javaClass != other.javaClass) {
return false
}
val that = other as GestureAlert
if (alertType != that.alertType) {
return false
}
return if (message != null) message == that.message else that.message == null
}
override fun hashCode(): Int {
var result = alertType
result = 31 * result + (message?.hashCode() ?: 0)
return result
}
companion object {
const val TYPE_NONE = 0
const val TYPE_START = 1
const val TYPE_END = 2
const val TYPE_PROGRESS = 3
const val TYPE_OTHER = 4
}
init {
when (alertType) {
TYPE_NONE -> color = android.R.color.black
TYPE_END -> color = android.R.color.holo_red_dark
TYPE_OTHER -> color = android.R.color.holo_purple
TYPE_PROGRESS -> color = android.R.color.holo_orange_dark
TYPE_START -> color = android.R.color.holo_green_dark
}
}
}
companion object {
private const val MAX_NUMBER_OF_ALERTS = 30
}
}