Skip to content

Instantly share code, notes, and snippets.

@marcouberti
Last active April 24, 2020 12:07
Show Gist options
  • Save marcouberti/4c5b5cb793e8ca796bfef14aaea829cc to your computer and use it in GitHub Desktop.
Save marcouberti/4c5b5cb793e8ca796bfef14aaea829cc to your computer and use it in GitHub Desktop.
Android Global Multitouch Events Interceptor. This multitouch events interceptor can be plugged into your app without directly coupling it with your activities.
/**
* This class is register to the [Application] activity lifecycle and
* add or remove the overlay view to each [Activity] as a content view.
*
* @see Activity.addContentView
*/
class SecretMenuGlobalTouchListener(
application: Application,
private val touchManager: SecretMenuTouchManager
): InvisibleOverlayView.TouchListener {
init {
application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStarted(activity: Activity) {
createOverlay(activity)
}
override fun onActivityDestroyed(activity: Activity) {
removeOverlay(activity)
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityResumed(activity: Activity) {}
})
}
override fun onTouch(event: MotionEvent): Boolean {
touchManager.onTouchEvent(event)
return true
}
private fun removeOverlay(activity: Activity) {
activity.window.decorView
.findViewById<InvisibleOverlayView>(R.id.secret_menu_invisible_overlay)?.let {
(it.parent as? ViewGroup)?.removeView(it)
}
}
private fun createOverlay(activity: Activity) {
val params = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
val view = activity.window.decorView.findViewById(R.id.secret_menu_invisible_overlay)
?: activity.layoutInflater
.inflate(R.layout.secret_menu_invisible_overlay, null)
.also { activity.addContentView(it, params) } as InvisibleOverlayView
view.activity = activity
view.setTouchListener(this)
}
}
class MyApplication : Application(), SecretMenuTouchManagerListener {
override fun onCreate() {
super.onCreate()
val touchManager = SecretMenuTouchManager(this)
val globalTouchListener = SecretMenuGlobalTouchListener(touchManager)
}
fun onActivateSecretMenu() {
// do your stuff here
}
}
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.FrameLayout
/**
* This is the invisible [ViewGroup] overlay that intercepts
* all the multitouch events.
*/
class InvisibleOverlayView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
var activity: Activity? = null
private var listener: TouchListener? = null
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
// handle the motion event
listener?.onTouch(event)
// but dispatch it to other sibling view group
val parentViewGroup = this.parent as? ViewGroup
parentViewGroup?.let { parent ->
val childCount = parent.childCount
for(i in 0..childCount) {
val view = parent.getChildAt(i)
if(view != this) {
view?.dispatchTouchEvent(event)
} else {
// skip myself
}
}
}
// important to return true here, otherwise
// multi touch events will not be intercepted, but only ACTION_DOWN
return true
}
fun setTouchListener(listener: TouchListener) {
this.listener = listener
}
interface TouchListener {
fun onTouch(event: MotionEvent): Boolean
}
}
<?xml version="1.0" encoding="utf-8"?>
<com.example.InvisibleOverlayView
android:id="@+id/secret_menu_invisible_overlay"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@android:color/transparent"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
/**
* The class responsible to handle the user touch events and decide if
* the secret menu should be opened or not.
*
* @param listener a listener to call when the secret menu should be opened.
* @param config the secret menu configuration.
*/
class SecretMenuTouchManager(
private val listener: SecretMenuTouchManagerListener
) {
companion object {
private const val FINGERS_COUNT = 2
private const val DELAY = 2000L
}
private var timerJob: Job? = null
/**
* When the user hold the fingers down a timer starts
* and when it finishes the secret menu opens.
* If the user releases the fingers before the timer ends
* the timer is cancelled and nothing happens.
*/
fun onTouchEvent(ev: MotionEvent) {
if(ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_POINTER_DOWN && ev.pointerCount == FINGERS_COUNT) {
startTimer()
} else if(ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_POINTER_UP || ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_UP) {
cancelTimer()
}
}
private fun cancelTimer() {
timerJob?.cancel()
}
private fun startTimer() {
timerJob?.cancel()
timerJob = GlobalScope.launch(Dispatchers.Main) {
delay(DELAY)
onTimerEnd()
}
}
private fun onTimerEnd() {
activateSecretMenu()
}
private fun activateSecretMenu() {
listener.onActivateSecretMenu()
}
}
/**
* Listens to [SecretMenuTouchManager] events.
*/
interface SecretMenuTouchManagerListener {
/**
* Invoked when the [SecretMenuTouchManager] decides the secret menu must be opened.
*/
fun onActivateSecretMenu()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment