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