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