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