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.binder 18 19 import android.content.res.Configuration 20 import android.graphics.Bitmap 21 import android.graphics.Matrix 22 import android.graphics.Rect 23 import android.util.LayoutDirection 24 import android.view.LayoutInflater 25 import android.view.View 26 import android.view.ViewGroup 27 import android.widget.FrameLayout 28 import android.widget.ImageView 29 import android.widget.LinearLayout 30 import androidx.lifecycle.Lifecycle 31 import androidx.lifecycle.lifecycleScope 32 import androidx.lifecycle.repeatOnLifecycle 33 import com.android.systemui.lifecycle.repeatWhenAttached 34 import com.android.systemui.res.R 35 import com.android.systemui.screenshot.ScreenshotEvent 36 import com.android.systemui.screenshot.ui.ScreenshotAnimationController 37 import com.android.systemui.screenshot.ui.ScreenshotShelfView 38 import com.android.systemui.screenshot.ui.SwipeGestureListener 39 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel 40 import com.android.systemui.screenshot.ui.viewmodel.AnimationState 41 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel 42 import com.android.systemui.util.children 43 import javax.inject.Inject 44 import kotlinx.coroutines.Dispatchers 45 import kotlinx.coroutines.launch 46 47 class ScreenshotShelfViewBinder 48 @Inject 49 constructor(private val buttonViewBinder: ActionButtonViewBinder) { 50 fun bind( 51 view: ScreenshotShelfView, 52 viewModel: ScreenshotViewModel, 53 animationController: ScreenshotAnimationController, 54 layoutInflater: LayoutInflater, 55 onDismissalRequested: (event: ScreenshotEvent, velocity: Float?) -> Unit, 56 onUserInteraction: () -> Unit 57 ) { 58 val swipeGestureListener = 59 SwipeGestureListener( 60 view, 61 onDismiss = { 62 onDismissalRequested(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, it) 63 }, 64 onCancel = { animationController.getSwipeReturnAnimation().start() } 65 ) 66 view.onTouchInterceptListener = { swipeGestureListener.onMotionEvent(it) } 67 view.userInteractionCallback = onUserInteraction 68 69 val previewView: ImageView = view.requireViewById(R.id.screenshot_preview) 70 val previewViewBlur: ImageView = view.requireViewById(R.id.screenshot_preview_blur) 71 val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border) 72 previewView.clipToOutline = true 73 previewViewBlur.clipToOutline = true 74 val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions) 75 val dismissButton = view.requireViewById<View>(R.id.screenshot_dismiss_button) 76 dismissButton.visibility = if (viewModel.showDismissButton) View.VISIBLE else View.GONE 77 dismissButton.setOnClickListener { 78 onDismissalRequested(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL, null) 79 } 80 val scrollingScrim: ImageView = view.requireViewById(R.id.screenshot_scrolling_scrim) 81 val scrollablePreview: ImageView = view.requireViewById(R.id.screenshot_scrollable_preview) 82 val badgeView = view.requireViewById<ImageView>(R.id.screenshot_badge) 83 84 // use immediate dispatcher to ensure screenshot bitmap is set before animation 85 view.repeatWhenAttached(Dispatchers.Main.immediate) { 86 lifecycleScope.launch { 87 repeatOnLifecycle(Lifecycle.State.STARTED) { 88 launch { 89 viewModel.preview.collect { bitmap -> 90 if (bitmap != null) { 91 setScreenshotBitmap(previewView, bitmap) 92 setScreenshotBitmap(previewViewBlur, bitmap) 93 previewView.visibility = View.VISIBLE 94 previewBorder.visibility = View.VISIBLE 95 } else { 96 previewView.visibility = View.GONE 97 previewBorder.visibility = View.GONE 98 } 99 } 100 } 101 launch { 102 viewModel.scrollingScrim.collect { bitmap -> 103 if (bitmap != null) { 104 scrollingScrim.setImageBitmap(bitmap) 105 scrollingScrim.visibility = View.VISIBLE 106 } else { 107 scrollingScrim.visibility = View.GONE 108 } 109 } 110 } 111 launch { 112 viewModel.scrollableRect.collect { rect -> 113 if (rect != null) { 114 setScrollablePreview( 115 scrollablePreview, 116 viewModel.preview.value, 117 rect 118 ) 119 } else { 120 scrollablePreview.visibility = View.GONE 121 } 122 } 123 } 124 launch { 125 viewModel.badge.collect { badge -> 126 badgeView.setImageDrawable(badge) 127 badgeView.visibility = if (badge != null) View.VISIBLE else View.GONE 128 } 129 } 130 launch { 131 viewModel.previewAction.collect { action -> 132 previewView.setOnClickListener { action?.onClick?.invoke() } 133 previewView.contentDescription = action?.contentDescription 134 } 135 } 136 launch { 137 viewModel.isAnimating.collect { isAnimating -> 138 previewView.isClickable = !isAnimating 139 for (child in actionsContainer.children) { 140 child.isClickable = !isAnimating 141 } 142 } 143 } 144 launch { 145 viewModel.actions.collect { actions -> 146 updateActions( 147 actions, 148 viewModel.animationState.value, 149 view, 150 layoutInflater 151 ) 152 } 153 } 154 launch { 155 viewModel.animationState.collect { animationState -> 156 updateActions( 157 viewModel.actions.value, 158 animationState, 159 view, 160 layoutInflater 161 ) 162 } 163 } 164 } 165 } 166 } 167 } 168 169 private fun updateActions( 170 actions: List<ActionButtonViewModel>, 171 animationState: AnimationState, 172 view: ScreenshotShelfView, 173 layoutInflater: LayoutInflater 174 ) { 175 val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions) 176 val visibleActions = 177 actions.filter { 178 it.visible && 179 (animationState == AnimationState.ENTRANCE_COMPLETE || 180 animationState == AnimationState.ENTRANCE_REVEAL || 181 it.showDuringEntrance) 182 } 183 184 if (visibleActions.isNotEmpty()) { 185 view.requireViewById<View>(R.id.actions_container_background).visibility = View.VISIBLE 186 } 187 188 // Remove any buttons not in the new list, then do another pass to add 189 // any new actions and update any that are already there. 190 // This assumes that actions can never change order and that each action 191 // ID is unique. 192 val newIds = visibleActions.map { it.id } 193 194 for (child in actionsContainer.children.toList()) { 195 if (child.tag !in newIds) { 196 actionsContainer.removeView(child) 197 } 198 } 199 200 for ((index, action) in visibleActions.withIndex()) { 201 val currentView: View? = actionsContainer.getChildAt(index) 202 if (action.id == currentView?.tag) { 203 // Same ID, update the display 204 buttonViewBinder.bind(currentView, action) 205 } else { 206 // Different ID. Removals have already happened so this must 207 // mean that the new action must be inserted here. 208 val actionButton = 209 layoutInflater.inflate(R.layout.shelf_action_chip, actionsContainer, false) 210 actionsContainer.addView(actionButton, index) 211 buttonViewBinder.bind(actionButton, action) 212 } 213 } 214 } 215 216 private fun setScreenshotBitmap(screenshotPreview: ImageView, bitmap: Bitmap) { 217 screenshotPreview.setImageBitmap(bitmap) 218 val hasPortraitAspectRatio = bitmap.width < bitmap.height 219 val fixedSize = screenshotPreview.resources.getDimensionPixelSize(R.dimen.overlay_x_scale) 220 val params: ViewGroup.LayoutParams = screenshotPreview.layoutParams 221 if (hasPortraitAspectRatio) { 222 params.width = fixedSize 223 params.height = FrameLayout.LayoutParams.WRAP_CONTENT 224 screenshotPreview.scaleType = ImageView.ScaleType.FIT_START 225 } else { 226 params.width = FrameLayout.LayoutParams.WRAP_CONTENT 227 params.height = fixedSize 228 screenshotPreview.scaleType = ImageView.ScaleType.FIT_END 229 } 230 231 screenshotPreview.layoutParams = params 232 screenshotPreview.requestLayout() 233 } 234 235 private fun setScrollablePreview( 236 scrollablePreview: ImageView, 237 bitmap: Bitmap?, 238 scrollableRect: Rect 239 ) { 240 if (bitmap == null) { 241 return 242 } 243 val fixedSize = scrollablePreview.resources.getDimensionPixelSize(R.dimen.overlay_x_scale) 244 val inPortrait = 245 scrollablePreview.resources.configuration.orientation == 246 Configuration.ORIENTATION_PORTRAIT 247 val scale: Float = fixedSize / ((if (inPortrait) bitmap.width else bitmap.height).toFloat()) 248 val params = scrollablePreview.layoutParams 249 250 params.width = (scale * scrollableRect.width()).toInt() 251 params.height = (scale * scrollableRect.height()).toInt() 252 val matrix = Matrix() 253 matrix.setScale(scale, scale) 254 matrix.postTranslate(-scrollableRect.left * scale, -scrollableRect.top * scale) 255 256 scrollablePreview.translationX = 257 (scale * 258 if (scrollablePreview.layoutDirection == LayoutDirection.LTR) scrollableRect.left 259 else scrollableRect.right - (scrollablePreview.parent as View).width) 260 scrollablePreview.translationY = scale * scrollableRect.top 261 scrollablePreview.setImageMatrix(matrix) 262 scrollablePreview.setImageBitmap(bitmap) 263 scrollablePreview.setVisibility(View.VISIBLE) 264 } 265 } 266