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.view
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.shared.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      */
getContentTranslationnull51     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      */
transformScrollXnull63     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 
onLayoutnull77     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
78         if (!isLaidOut && isLayoutRtl) {
79             // Reset scroll because onLayout method overrides RTL scroll if view was not laid out.
80             mScrollX = relativeScrollX
81         }
82         super.onLayout(changed, l, t, r, b)
83     }
84 
85     /** Allow all scrolls to go through, use base implementation */
scrollTonull86     override fun scrollTo(x: Int, y: Int) {
87         if (mScrollX != x || mScrollY != y) {
88             val oldX: Int = mScrollX
89             val oldY: Int = mScrollY
90             mScrollX = x
91             mScrollY = y
92             invalidateParentCaches()
93             onScrollChanged(mScrollX, mScrollY, oldX, oldY)
94             if (!awakenScrollBars()) {
95                 postInvalidateOnAnimation()
96             }
97         }
98     }
99 
onInterceptTouchEventnull100     override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
101         var intercept = false
102         touchListener?.let { intercept = it.onInterceptTouchEvent(ev) }
103         return super.onInterceptTouchEvent(ev) || intercept
104     }
105 
onTouchEventnull106     override fun onTouchEvent(ev: MotionEvent?): Boolean {
107         var touch = false
108         touchListener?.let { touch = it.onTouchEvent(ev) }
109         return super.onTouchEvent(ev) || touch
110     }
111 
onFinishInflatenull112     override fun onFinishInflate() {
113         super.onFinishInflate()
114         contentContainer = getChildAt(0) as ViewGroup
115     }
116 
overScrollBynull117     override fun overScrollBy(
118         deltaX: Int,
119         deltaY: Int,
120         scrollX: Int,
121         scrollY: Int,
122         scrollRangeX: Int,
123         scrollRangeY: Int,
124         maxOverScrollX: Int,
125         maxOverScrollY: Int,
126         isTouchEvent: Boolean
127     ): Boolean {
128         if (getContentTranslation() != 0.0f) {
129             // When we're dismissing we ignore all the scrolling
130             return false
131         }
132         return super.overScrollBy(
133             deltaX,
134             deltaY,
135             scrollX,
136             scrollY,
137             scrollRangeX,
138             scrollRangeY,
139             maxOverScrollX,
140             maxOverScrollY,
141             isTouchEvent
142         )
143     }
144 
145     /** Cancel the current touch event going on. */
cancelCurrentScrollnull146     fun cancelCurrentScroll() {
147         val now = SystemClock.uptimeMillis()
148         val event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
149         event.source = InputDevice.SOURCE_TOUCHSCREEN
150         super.onTouchEvent(event)
151         event.recycle()
152     }
153 }
154