1 /* <lambda>null2 * Copyright (C) 2024 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.screenshot.ui 18 19 import android.content.Context 20 import android.content.res.Configuration 21 import android.graphics.Insets 22 import android.graphics.Rect 23 import android.graphics.Region 24 import android.util.AttributeSet 25 import android.view.GestureDetector 26 import android.view.GestureDetector.SimpleOnGestureListener 27 import android.view.MotionEvent 28 import android.view.View 29 import android.view.ViewGroup 30 import android.view.WindowInsets 31 import android.widget.FrameLayout 32 import android.widget.ImageView 33 import com.android.systemui.res.R 34 import com.android.systemui.screenshot.FloatingWindowUtil 35 36 class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : 37 FrameLayout(context, attrs) { 38 lateinit var screenshotPreview: ImageView 39 lateinit var blurredScreenshotPreview: ImageView 40 private lateinit var screenshotStatic: ViewGroup 41 var onTouchInterceptListener: ((MotionEvent) -> Boolean)? = null 42 43 var userInteractionCallback: (() -> Unit)? = null 44 45 private val displayMetrics = context.resources.displayMetrics 46 private val tmpRect = Rect() 47 private lateinit var actionsContainerBackground: View 48 private lateinit var actionsContainer: View 49 private lateinit var dismissButton: View 50 51 // Prepare an internal `GestureDetector` to determine when we can initiate a touch-interception 52 // session (with the client's provided `onTouchInterceptListener`). We delegate out to their 53 // listener only for gestures that can't be handled by scrolling our `actionsContainer`. 54 private val gestureDetector = 55 GestureDetector( 56 context, 57 object : SimpleOnGestureListener() { 58 override fun onScroll( 59 ev1: MotionEvent?, 60 ev2: MotionEvent, 61 distanceX: Float, 62 distanceY: Float 63 ): Boolean { 64 actionsContainer.getBoundsOnScreen(tmpRect) 65 val touchedInActionsContainer = 66 tmpRect.contains(ev2.rawX.toInt(), ev2.rawY.toInt()) 67 val canHandleInternallyByScrolling = 68 touchedInActionsContainer 69 && actionsContainer.canScrollHorizontally(distanceX.toInt()) 70 return !canHandleInternallyByScrolling 71 } 72 } 73 ) 74 75 init { 76 77 // Delegate to the client-provided `onTouchInterceptListener` if we've already initiated 78 // touch-interception. 79 setOnTouchListener({ _: View, ev: MotionEvent -> 80 userInteractionCallback?.invoke() 81 onTouchInterceptListener?.invoke(ev) ?: false 82 }) 83 84 gestureDetector.setIsLongpressEnabled(false) 85 } 86 87 override fun onFinishInflate() { 88 super.onFinishInflate() 89 // Get focus so that the key events go to the layout. 90 isFocusableInTouchMode = true 91 screenshotPreview = requireViewById(R.id.screenshot_preview) 92 blurredScreenshotPreview = requireViewById(R.id.screenshot_preview_blur) 93 screenshotStatic = requireViewById(R.id.screenshot_static) 94 actionsContainerBackground = requireViewById(R.id.actions_container_background) 95 actionsContainer = requireViewById(R.id.actions_container) 96 dismissButton = requireViewById(R.id.screenshot_dismiss_button) 97 98 // Configure to extend the timeout during ongoing gestures (i.e. scrolls) that are already 99 // being handled by our child views. 100 actionsContainer.setOnTouchListener({ _: View, ev: MotionEvent -> 101 userInteractionCallback?.invoke() 102 false 103 }) 104 } 105 106 fun getTouchRegion(gestureInsets: Insets): Region { 107 val region = getSwipeRegion() 108 109 // Receive touches in gesture insets so they don't cause TOUCH_OUTSIDE 110 // left edge gesture region 111 val insetRect = Rect(0, 0, gestureInsets.left, displayMetrics.heightPixels) 112 region.op(insetRect, Region.Op.UNION) 113 // right edge gesture region 114 insetRect.set( 115 displayMetrics.widthPixels - gestureInsets.right, 116 0, 117 displayMetrics.widthPixels, 118 displayMetrics.heightPixels 119 ) 120 region.op(insetRect, Region.Op.UNION) 121 122 return region 123 } 124 125 fun updateInsets(insets: WindowInsets) { 126 val orientation = mContext.resources.configuration.orientation 127 val inPortrait = orientation == Configuration.ORIENTATION_PORTRAIT 128 val cutout = insets.displayCutout 129 val navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()) 130 131 // When honoring the navbar or other obstacle offsets, include some extra padding above 132 // the inset itself. 133 val verticalPadding = 134 mContext.resources.getDimensionPixelOffset(R.dimen.screenshot_shelf_vertical_margin) 135 136 // Minimum bottom padding to always enforce (e.g. if there's no nav bar) 137 val minimumBottomPadding = 138 context.resources.getDimensionPixelOffset( 139 R.dimen.overlay_action_container_minimum_edge_spacing 140 ) 141 142 if (cutout == null) { 143 screenshotStatic.setPadding(0, 0, 0, navBarInsets.bottom) 144 } else { 145 val waterfall = cutout.waterfallInsets 146 if (inPortrait) { 147 screenshotStatic.setPadding( 148 waterfall.left, 149 max(cutout.safeInsetTop, waterfall.top), 150 waterfall.right, 151 max( 152 navBarInsets.bottom + verticalPadding, 153 cutout.safeInsetBottom + verticalPadding, 154 waterfall.bottom + verticalPadding, 155 minimumBottomPadding, 156 ) 157 ) 158 } else { 159 screenshotStatic.setPadding( 160 max(cutout.safeInsetLeft, waterfall.left), 161 waterfall.top, 162 max(cutout.safeInsetRight, waterfall.right), 163 max( 164 navBarInsets.bottom + verticalPadding, 165 waterfall.bottom + verticalPadding, 166 minimumBottomPadding, 167 ) 168 ) 169 } 170 } 171 } 172 173 // Max function for two or more params. 174 private fun max(first: Int, second: Int, vararg items: Int): Int { 175 var largest = if (first > second) first else second 176 for (item in items) { 177 if (item > largest) { 178 largest = item 179 } 180 } 181 return largest 182 } 183 184 private fun getSwipeRegion(): Region { 185 val swipeRegion = Region() 186 val padding = FloatingWindowUtil.dpToPx(displayMetrics, -1 * TOUCH_PADDING_DP).toInt() 187 swipeRegion.addInsetView(screenshotPreview, padding) 188 swipeRegion.addInsetView(actionsContainerBackground, padding) 189 swipeRegion.addInsetView(dismissButton, padding) 190 findViewById<View>(R.id.screenshot_message_container)?.let { 191 swipeRegion.addInsetView(it, padding) 192 } 193 return swipeRegion 194 } 195 196 private fun Region.addInsetView(view: View, padding: Int = 0) { 197 view.getBoundsOnScreen(tmpRect) 198 tmpRect.inset(padding, padding) 199 this.op(tmpRect, Region.Op.UNION) 200 } 201 202 companion object { 203 private const val TOUCH_PADDING_DP = 12f 204 } 205 206 override fun onInterceptHoverEvent(event: MotionEvent): Boolean { 207 userInteractionCallback?.invoke() 208 return super.onInterceptHoverEvent(event) 209 } 210 211 override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { 212 userInteractionCallback?.invoke() 213 214 // Let the client-provided listener see all `DOWN` events so that they'll be able to 215 // interpret the remainder of the gesture, even if interception starts partway-through. 216 // TODO: is this really necessary? And if we don't go on to start interception, should we 217 // follow up with `ACTION_CANCEL`? 218 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 219 onTouchInterceptListener?.invoke(ev) 220 } 221 222 // Only allow the client-provided touch interceptor to take over the gesture if our 223 // top-level `GestureDetector` decides not to scroll the action container. 224 return gestureDetector.onTouchEvent(ev) 225 } 226 } 227