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