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