1 /*
2  * Copyright (C) 2022 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.systemui.media.controls.ui
18 
19 import android.content.Context
20 import android.os.SystemClock
21 import android.util.AttributeSet
22 import android.view.InputDevice
23 import android.view.MotionEvent
24 import android.view.ViewGroup
25 import android.widget.HorizontalScrollView
26 import com.android.systemui.Gefingerpoken
27 import com.android.wm.shell.animation.physicsAnimator
28 
29 /**
30  * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful when
31  * only measuring children but not the parent, when trying to apply a new scroll position
32  */
33 class MediaScrollView
34 @JvmOverloads
35 constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
36     HorizontalScrollView(context, attrs, defStyleAttr) {
37 
38     lateinit var contentContainer: ViewGroup
39         private set
40     var touchListener: Gefingerpoken? = null
41 
42     /**
43      * The target value of the translation X animation. Only valid if the physicsAnimator is running
44      */
45     var animationTargetX = 0.0f
46 
47     /**
48      * Get the current content translation. This is usually the normal translationX of the content,
49      * but when animating, it might differ
50      */
51     fun getContentTranslation() =
52         if (contentContainer.physicsAnimator.isRunning()) {
53             animationTargetX
54         } else {
55             contentContainer.translationX
56         }
57 
58     /**
59      * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media
60      * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX is
61      * always absolute. This function is its own inverse.
62      */
63     private fun transformScrollX(scrollX: Int): Int =
64         if (isLayoutRtl) {
65             contentContainer.width - width - scrollX
66         } else {
67             scrollX
68         }
69 
70     /** Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. */
71     var relativeScrollX: Int
72         get() = transformScrollX(scrollX)
73         set(value) {
74             scrollX = transformScrollX(value)
75         }
76 
77     /** Allow all scrolls to go through, use base implementation */
78     override fun scrollTo(x: Int, y: Int) {
79         if (mScrollX != x || mScrollY != y) {
80             val oldX: Int = mScrollX
81             val oldY: Int = mScrollY
82             mScrollX = x
83             mScrollY = y
84             invalidateParentCaches()
85             onScrollChanged(mScrollX, mScrollY, oldX, oldY)
86             if (!awakenScrollBars()) {
87                 postInvalidateOnAnimation()
88             }
89         }
90     }
91 
92     override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
93         var intercept = false
94         touchListener?.let { intercept = it.onInterceptTouchEvent(ev) }
95         return super.onInterceptTouchEvent(ev) || intercept
96     }
97 
98     override fun onTouchEvent(ev: MotionEvent?): Boolean {
99         var touch = false
100         touchListener?.let { touch = it.onTouchEvent(ev) }
101         return super.onTouchEvent(ev) || touch
102     }
103 
104     override fun onFinishInflate() {
105         super.onFinishInflate()
106         contentContainer = getChildAt(0) as ViewGroup
107     }
108 
109     override fun overScrollBy(
110         deltaX: Int,
111         deltaY: Int,
112         scrollX: Int,
113         scrollY: Int,
114         scrollRangeX: Int,
115         scrollRangeY: Int,
116         maxOverScrollX: Int,
117         maxOverScrollY: Int,
118         isTouchEvent: Boolean
119     ): Boolean {
120         if (getContentTranslation() != 0.0f) {
121             // When we're dismissing we ignore all the scrolling
122             return false
123         }
124         return super.overScrollBy(
125             deltaX,
126             deltaY,
127             scrollX,
128             scrollY,
129             scrollRangeX,
130             scrollRangeY,
131             maxOverScrollX,
132             maxOverScrollY,
133             isTouchEvent
134         )
135     }
136 
137     /** Cancel the current touch event going on. */
138     fun cancelCurrentScroll() {
139         val now = SystemClock.uptimeMillis()
140         val event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
141         event.source = InputDevice.SOURCE_TOUCHSCREEN
142         super.onTouchEvent(event)
143         event.recycle()
144     }
145 }
146