1 /* 2 * Copyright (C) 2023 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 18 package com.android.systemui.common.ui.view 19 20 import android.annotation.SuppressLint 21 import android.content.Context 22 import android.util.AttributeSet 23 import android.view.MotionEvent 24 import android.view.View 25 import com.android.systemui.shade.TouchLogger 26 import kotlin.math.pow 27 import kotlin.math.sqrt 28 import kotlinx.coroutines.DisposableHandle 29 30 /** 31 * View designed to handle long-presses. 32 * 33 * The view will not handle any long pressed by default. To set it up, set up a listener and, when 34 * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`. 35 */ 36 class LongPressHandlingView( 37 context: Context, 38 attrs: AttributeSet?, 39 ) : 40 View( 41 context, 42 attrs, 43 ) { 44 interface Listener { 45 /** Notifies that a long-press has been detected by the given view. */ 46 fun onLongPressDetected( 47 view: View, 48 x: Int, 49 y: Int, 50 ) 51 52 /** Notifies that the gesture was too short for a long press, it is actually a click. */ 53 fun onSingleTapDetected(view: View) = Unit 54 } 55 56 var listener: Listener? = null 57 58 private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy { 59 LongPressHandlingViewInteractionHandler( 60 postDelayed = { block, timeoutMs -> 61 val dispatchToken = Any() 62 63 handler.postDelayed( 64 block, 65 dispatchToken, 66 timeoutMs, 67 ) 68 69 DisposableHandle { handler.removeCallbacksAndMessages(dispatchToken) } 70 }, 71 isAttachedToWindow = ::isAttachedToWindow, 72 onLongPressDetected = { x, y -> 73 listener?.onLongPressDetected( 74 view = this, 75 x = x, 76 y = y, 77 ) 78 }, 79 onSingleTapDetected = { listener?.onSingleTapDetected(this@LongPressHandlingView) }, 80 ) 81 } 82 83 fun setLongPressHandlingEnabled(isEnabled: Boolean) { 84 interactionHandler.isLongPressHandlingEnabled = isEnabled 85 } 86 87 override fun dispatchTouchEvent(event: MotionEvent): Boolean { 88 return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event)) 89 } 90 91 @SuppressLint("ClickableViewAccessibility") 92 override fun onTouchEvent(event: MotionEvent?): Boolean { 93 return interactionHandler.onTouchEvent(event?.toModel()) 94 } 95 } 96 97 private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel { 98 return when (actionMasked) { 99 MotionEvent.ACTION_DOWN -> 100 LongPressHandlingViewInteractionHandler.MotionEventModel.Down( 101 x = x.toInt(), 102 y = y.toInt(), 103 ) 104 MotionEvent.ACTION_MOVE -> 105 LongPressHandlingViewInteractionHandler.MotionEventModel.Move( 106 distanceMoved = distanceMoved(), 107 ) 108 MotionEvent.ACTION_UP -> 109 LongPressHandlingViewInteractionHandler.MotionEventModel.Up( 110 distanceMoved = distanceMoved(), 111 gestureDuration = gestureDuration(), 112 ) 113 MotionEvent.ACTION_CANCEL -> LongPressHandlingViewInteractionHandler.MotionEventModel.Cancel 114 else -> LongPressHandlingViewInteractionHandler.MotionEventModel.Other 115 } 116 } 117 118 private fun MotionEvent.distanceMoved(): Float { 119 return if (historySize > 0) { 120 sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2)) 121 } else { 122 0f 123 } 124 } 125 126 private fun MotionEvent.gestureDuration(): Long { 127 return eventTime - downTime 128 } 129