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 }