1 /*
<lambda>null2  * Copyright (C) 2024 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 package com.android.wallpaper.picker.preview.ui.view
18 
19 import android.content.Context
20 import android.graphics.Rect
21 import android.util.AttributeSet
22 import android.view.GestureDetector
23 import android.view.GestureDetector.SimpleOnGestureListener
24 import android.view.MotionEvent
25 import android.view.ViewGroup
26 import android.view.ViewParent
27 import androidx.constraintlayout.motion.widget.MotionLayout
28 import androidx.core.view.ancestors
29 import androidx.core.view.children
30 
31 /** A [MotionLayout] that performs click on one of its child if it is the recipient. */
32 class ClickableMotionLayout(context: Context, attrs: AttributeSet?) : MotionLayout(context, attrs) {
33 
34     /** True for this view to intercept all motion events. */
35     var shouldInterceptTouch = true
36 
37     private val clickableViewIds = mutableListOf<Int>()
38     private val singleTapDetector =
39         GestureDetector(
40             context,
41             object : SimpleOnGestureListener() {
42                 override fun onSingleTapUp(event: MotionEvent): Boolean {
43                     // Check if any immediate child view is clicked
44                     children
45                         .find {
46                             isEventPointerInRect(event, Rect(it.left, it.top, it.right, it.bottom))
47                         }
48                         ?.let { child ->
49                             // Find all the clickable ids in the hierarchy of the clicked view and
50                             // perform click on the exact view that should be clicked.
51                             clickableViewIds
52                                 .mapNotNull { child.findViewById(it) }
53                                 .find { clickableView ->
54                                     if (clickableView == child) {
55                                         true
56                                     } else {
57                                         // Find ancestors of this clickable view up until this
58                                         // layout and transform coordinates to align with motion
59                                         // event.
60                                         val ancestors = clickableView.ancestors
61                                         var ancestorsLeft = 0
62                                         var ancestorsTop = 0
63                                         ancestors
64                                             .filter {
65                                                 ancestors.indexOf(it) <=
66                                                     ancestors.indexOf(child as ViewParent)
67                                             }
68                                             .forEach {
69                                                 it as ViewGroup
70                                                 ancestorsLeft += it.left
71                                                 ancestorsTop += it.top
72                                             }
73                                         isEventPointerInRect(
74                                             event,
75                                             Rect(
76                                                 /* left= */ ancestorsLeft + clickableView.left,
77                                                 /* top= */ ancestorsTop + clickableView.top,
78                                                 /* right= */ ancestorsLeft + clickableView.right,
79                                                 /* bottom= */ ancestorsTop + clickableView.bottom,
80                                             ),
81                                         )
82                                     }
83                                 }
84                                 ?.performClick()
85                         }
86 
87                     return true
88                 }
89             },
90         )
91 
92     override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
93         // MotionEvent.ACTION_DOWN is the first MotionEvent received and is necessary to detect
94         // various gesture, returns true to intercept all event so they are forwarded into
95         // onTouchEvent.
96         return shouldInterceptTouch
97     }
98 
99     override fun onTouchEvent(event: MotionEvent): Boolean {
100         super.onTouchEvent(event)
101 
102         // Handle single tap
103         singleTapDetector.onTouchEvent(event)
104 
105         return true
106     }
107 
108     fun setClickableViewIds(ids: List<Int>) {
109         clickableViewIds.apply {
110             clear()
111             addAll(ids)
112         }
113     }
114 
115     private fun isEventPointerInRect(e: MotionEvent, rect: Rect): Boolean {
116         return e.x >= rect.left && e.x <= rect.right && e.y >= rect.top && e.y <= rect.bottom
117     }
118 }
119