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.wallpaper.picker.customization.ui 18 19 import android.graphics.Color 20 import android.graphics.Point 21 import android.os.Bundle 22 import android.view.View 23 import android.widget.FrameLayout 24 import android.widget.LinearLayout 25 import androidx.activity.OnBackPressedCallback 26 import androidx.activity.viewModels 27 import androidx.appcompat.app.AppCompatActivity 28 import androidx.constraintlayout.motion.widget.MotionLayout 29 import androidx.constraintlayout.motion.widget.MotionLayout.TransitionListener 30 import androidx.constraintlayout.widget.ConstraintLayout 31 import androidx.constraintlayout.widget.Guideline 32 import androidx.core.view.WindowCompat 33 import androidx.core.view.doOnLayout 34 import androidx.core.view.doOnPreDraw 35 import androidx.recyclerview.widget.RecyclerView 36 import androidx.viewpager2.widget.ViewPager2 37 import com.android.wallpaper.R 38 import com.android.wallpaper.model.Screen 39 import com.android.wallpaper.model.Screen.HOME_SCREEN 40 import com.android.wallpaper.model.Screen.LOCK_SCREEN 41 import com.android.wallpaper.module.MultiPanesChecker 42 import com.android.wallpaper.picker.customization.ui.binder.CustomizationOptionsBinder 43 import com.android.wallpaper.picker.customization.ui.binder.CustomizationPickerBinder2 44 import com.android.wallpaper.picker.customization.ui.util.CustomizationOptionUtil 45 import com.android.wallpaper.picker.customization.ui.util.CustomizationOptionUtil.CustomizationOption 46 import com.android.wallpaper.picker.customization.ui.view.adapter.PreviewPagerAdapter 47 import com.android.wallpaper.picker.customization.ui.view.transformer.PreviewPagerPageTransformer 48 import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationPickerViewModel2 49 import com.android.wallpaper.util.ActivityUtils 50 import dagger.hilt.android.AndroidEntryPoint 51 import javax.inject.Inject 52 53 @AndroidEntryPoint(AppCompatActivity::class) 54 class CustomizationPickerActivity2 : Hilt_CustomizationPickerActivity2() { 55 56 @Inject lateinit var multiPanesChecker: MultiPanesChecker 57 @Inject lateinit var customizationOptionUtil: CustomizationOptionUtil 58 @Inject lateinit var customizationOptionsBinder: CustomizationOptionsBinder 59 60 private var fullyCollapsed = false 61 62 private val customizationPickerViewModel: CustomizationPickerViewModel2 by viewModels() 63 64 override fun onCreate(savedInstanceState: Bundle?) { 65 super.onCreate(savedInstanceState) 66 if ( 67 multiPanesChecker.isMultiPanesEnabled(this) && 68 !ActivityUtils.isLaunchedFromSettingsTrampoline(intent) && 69 !ActivityUtils.isLaunchedFromSettingsRelated(intent) 70 ) { 71 // If the device supports multi panes, we check if the activity is launched by settings. 72 // If not, we need to start an intent to have settings launch the customization 73 // activity. In case it is a two-pane situation and the activity should be embedded in 74 // the settings app, instead of in the full screen. 75 val multiPanesIntent = multiPanesChecker.getMultiPanesIntent(intent) 76 ActivityUtils.startActivityForResultSafely( 77 this, /* activity */ 78 multiPanesIntent, 79 0, /* requestCode */ 80 ) 81 finish() 82 } 83 84 setContentView(R.layout.activity_cusomization_picker2) 85 WindowCompat.setDecorFitsSystemWindows(window, ActivityUtils.isSUWMode(this)) 86 87 val rootView = requireViewById<MotionLayout>(R.id.picker_motion_layout) 88 89 customizationOptionUtil.initBottomSheetContent( 90 rootView.requireViewById<FrameLayout>(R.id.customization_picker_bottom_sheet), 91 layoutInflater, 92 ) 93 rootView.setTransitionListener( 94 object : EmptyTransitionListener { 95 override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) { 96 if ( 97 currentId == R.id.expanded_header_primary || 98 currentId == R.id.collapsed_header_primary 99 ) { 100 rootView.setTransition(R.id.transition_primary) 101 } 102 } 103 } 104 ) 105 106 initPreviewPager() 107 108 val optionContainer = requireViewById<MotionLayout>(R.id.customization_option_container) 109 // The collapsed header height should be updated when option container's height is known 110 optionContainer.doOnPreDraw { 111 // The bottom navigation bar height 112 val navBarHeight = 113 resources.getIdentifier("navigation_bar_height", "dimen", "android").let { 114 if (it > 0) { 115 resources.getDimensionPixelSize(it) 116 } else 0 117 } 118 val collapsedHeaderHeight = rootView.height - optionContainer.height - navBarHeight 119 if ( 120 collapsedHeaderHeight > 121 resources.getDimensionPixelSize( 122 R.dimen.customization_picker_preview_header_collapsed_height 123 ) 124 ) { 125 rootView 126 .getConstraintSet(R.id.collapsed_header_primary) 127 ?.constrainHeight(R.id.preview_header, collapsedHeaderHeight) 128 rootView.setTransition(R.id.transition_primary) 129 } 130 } 131 132 val onBackPressed = 133 CustomizationPickerBinder2.bind( 134 view = rootView, 135 lockScreenCustomizationOptionEntries = initCustomizationOptionEntries(LOCK_SCREEN), 136 homeScreenCustomizationOptionEntries = initCustomizationOptionEntries(HOME_SCREEN), 137 viewModel = customizationPickerViewModel, 138 customizationOptionsBinder = customizationOptionsBinder, 139 lifecycleOwner = this, 140 navigateToPrimary = { 141 if (rootView.currentState == R.id.secondary) { 142 rootView.transitionToState( 143 if (fullyCollapsed) R.id.collapsed_header_primary 144 else R.id.expanded_header_primary 145 ) 146 } 147 }, 148 navigateToSecondary = { screen -> 149 if (rootView.currentState != R.id.secondary) { 150 setCustomizePickerBottomSheetContent(rootView, screen) { 151 fullyCollapsed = rootView.progress == 1.0f 152 rootView.transitionToState(R.id.secondary) 153 } 154 } 155 }, 156 ) 157 158 onBackPressedDispatcher.addCallback( 159 object : OnBackPressedCallback(true) { 160 override fun handleOnBackPressed() { 161 val isOnBackPressedHandled = onBackPressed() 162 if (!isOnBackPressedHandled) { 163 remove() 164 onBackPressedDispatcher.onBackPressed() 165 } 166 } 167 } 168 ) 169 } 170 171 override fun onDestroy() { 172 customizationOptionUtil.onDestroy() 173 super.onDestroy() 174 } 175 176 private fun initCustomizationOptionEntries( 177 screen: Screen, 178 ): List<Pair<CustomizationOption, View>> { 179 val optionEntriesContainer = 180 requireViewById<LinearLayout>( 181 when (screen) { 182 LOCK_SCREEN -> R.id.lock_customization_option_container 183 HOME_SCREEN -> R.id.home_customization_option_container 184 } 185 ) 186 val optionEntries = 187 customizationOptionUtil.getOptionEntries(screen, optionEntriesContainer, layoutInflater) 188 optionEntries.onEachIndexed { index, (option, view) -> 189 val isFirst = index == 0 190 val isLast = index == optionEntries.size - 1 191 view.setBackgroundResource( 192 if (isFirst) R.drawable.customization_option_entry_top_background 193 else if (isLast) R.drawable.customization_option_entry_bottom_background 194 else R.drawable.customization_option_entry_background 195 ) 196 optionEntriesContainer.addView(view) 197 } 198 return optionEntries 199 } 200 201 private fun initPreviewPager() { 202 val pager = requireViewById<ViewPager2>(R.id.preview_pager) 203 pager.apply { 204 adapter = PreviewPagerAdapter { viewHolder, position -> 205 viewHolder.itemView 206 .requireViewById<View>(R.id.preview_card) 207 .setBackgroundColor(if (position == 0) Color.BLUE else Color.CYAN) 208 } 209 // Disable over scroll 210 (getChildAt(0) as RecyclerView).overScrollMode = RecyclerView.OVER_SCROLL_NEVER 211 // The neighboring view should be inflated when pager is rendered 212 offscreenPageLimit = 1 213 // When pager's height changes, request transform to recalculate the preview offset 214 // to make sure correct space between the previews. 215 addOnLayoutChangeListener { view, _, _, _, _, _, topWas, _, bottomWas -> 216 val isHeightChanged = (bottomWas - topWas) != view.height 217 if (isHeightChanged) { 218 pager.requestTransform() 219 } 220 } 221 } 222 223 // Only when pager is laid out, we can get the width and set the preview's offset correctly 224 pager.doOnLayout { 225 (it as ViewPager2).apply { 226 setPageTransformer(PreviewPagerPageTransformer(Point(width, height))) 227 } 228 } 229 } 230 231 private fun setCustomizePickerBottomSheetContent( 232 motionContainer: MotionLayout, 233 option: CustomizationOption, 234 onComplete: () -> Unit 235 ) { 236 val view = customizationOptionUtil.getBottomSheetContent(option) ?: return 237 238 val customizationBottomSheet = 239 requireViewById<FrameLayout>(R.id.customization_picker_bottom_sheet) 240 val guideline = requireViewById<Guideline>(R.id.preview_guideline_in_secondary_screen) 241 customizationBottomSheet.removeAllViews() 242 customizationBottomSheet.addView(view) 243 244 view.doOnPreDraw { 245 val height = view.height 246 guideline.setGuidelineEnd(height) 247 customizationBottomSheet.translationY = 0.0f 248 customizationBottomSheet.alpha = 0.0f 249 // Update the motion container 250 motionContainer.getConstraintSet(R.id.expanded_header_primary)?.apply { 251 setTranslationY(R.id.customization_picker_bottom_sheet, 0.0f) 252 setAlpha(R.id.customization_picker_bottom_sheet, 0.0f) 253 constrainHeight( 254 R.id.customization_picker_bottom_sheet, 255 ConstraintLayout.LayoutParams.WRAP_CONTENT 256 ) 257 } 258 motionContainer.getConstraintSet(R.id.collapsed_header_primary)?.apply { 259 setTranslationY(R.id.customization_picker_bottom_sheet, 0.0f) 260 setAlpha(R.id.customization_picker_bottom_sheet, 0.0f) 261 constrainHeight( 262 R.id.customization_picker_bottom_sheet, 263 ConstraintLayout.LayoutParams.WRAP_CONTENT 264 ) 265 } 266 motionContainer.getConstraintSet(R.id.secondary)?.apply { 267 setGuidelineEnd(R.id.preview_guideline_in_secondary_screen, height) 268 setTranslationY(R.id.customization_picker_bottom_sheet, -height.toFloat()) 269 setAlpha(R.id.customization_picker_bottom_sheet, 1.0f) 270 constrainHeight( 271 R.id.customization_picker_bottom_sheet, 272 ConstraintLayout.LayoutParams.WRAP_CONTENT 273 ) 274 } 275 onComplete() 276 } 277 } 278 279 interface EmptyTransitionListener : TransitionListener { 280 override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) { 281 // Do nothing intended 282 } 283 284 override fun onTransitionChange( 285 motionLayout: MotionLayout?, 286 startId: Int, 287 endId: Int, 288 progress: Float 289 ) { 290 // Do nothing intended 291 } 292 293 override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) { 294 // Do nothing intended 295 } 296 297 override fun onTransitionTrigger( 298 motionLayout: MotionLayout?, 299 triggerId: Int, 300 positive: Boolean, 301 progress: Float 302 ) { 303 // Do nothing intended 304 } 305 } 306 } 307