1 /* <lambda>null2 * Copyright 2021 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 androidx.compose.ui.tooling 18 19 import android.annotation.SuppressLint 20 import android.content.Context 21 import android.graphics.Canvas 22 import android.graphics.DashPathEffect 23 import android.graphics.Paint 24 import android.os.Bundle 25 import android.util.AttributeSet 26 import android.util.Log 27 import android.widget.FrameLayout 28 import androidx.activity.OnBackPressedDispatcher 29 import androidx.activity.OnBackPressedDispatcherOwner 30 import androidx.activity.compose.LocalActivityResultRegistryOwner 31 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner 32 import androidx.activity.result.ActivityResultRegistry 33 import androidx.activity.result.ActivityResultRegistryOwner 34 import androidx.activity.result.contract.ActivityResultContract 35 import androidx.annotation.VisibleForTesting 36 import androidx.compose.runtime.Composable 37 import androidx.compose.runtime.Composition 38 import androidx.compose.runtime.CompositionLocalProvider 39 import androidx.compose.runtime.SideEffect 40 import androidx.compose.runtime.currentComposer 41 import androidx.compose.runtime.snapshots.Snapshot 42 import androidx.compose.ui.ExperimentalComposeUiApi 43 import androidx.compose.ui.graphics.Color 44 import androidx.compose.ui.graphics.toArgb 45 import androidx.compose.ui.layout.LayoutInfo 46 import androidx.compose.ui.platform.ComposeView 47 import androidx.compose.ui.platform.LocalFontFamilyResolver 48 import androidx.compose.ui.platform.LocalFontLoader 49 import androidx.compose.ui.platform.ViewRootForTest 50 import androidx.compose.ui.text.font.createFontFamilyResolver 51 import androidx.compose.ui.tooling.animation.AnimationSearch 52 import androidx.compose.ui.tooling.animation.PreviewAnimationClock 53 import androidx.compose.ui.tooling.data.Group 54 import androidx.compose.ui.tooling.data.NodeGroup 55 import androidx.compose.ui.tooling.data.SourceLocation 56 import androidx.compose.ui.tooling.data.UiToolingDataApi 57 import androidx.compose.ui.tooling.data.asTree 58 import androidx.compose.ui.tooling.preview.Preview 59 import androidx.compose.ui.tooling.preview.PreviewParameterProvider 60 import androidx.compose.ui.unit.IntRect 61 import androidx.core.app.ActivityOptionsCompat 62 import androidx.lifecycle.Lifecycle 63 import androidx.lifecycle.LifecycleRegistry 64 import androidx.lifecycle.ViewModelStore 65 import androidx.lifecycle.ViewModelStoreOwner 66 import androidx.lifecycle.setViewTreeLifecycleOwner 67 import androidx.lifecycle.setViewTreeViewModelStoreOwner 68 import androidx.savedstate.SavedStateRegistry 69 import androidx.savedstate.SavedStateRegistryController 70 import androidx.savedstate.SavedStateRegistryOwner 71 import androidx.savedstate.setViewTreeSavedStateRegistryOwner 72 import java.lang.reflect.Method 73 74 private const val TOOLS_NS_URI = "http://schemas.android.com/tools" 75 private const val DESIGN_INFO_METHOD = "getDesignInfo" 76 77 private const val REMEMBER = "remember" 78 79 private val emptyContent: @Composable () -> Unit = @Composable {} 80 81 /** 82 * Class containing the minimum information needed by the Preview to map components to the source 83 * code and render boundaries. 84 */ 85 @OptIn(UiToolingDataApi::class) 86 internal data class ViewInfo( 87 val fileName: String, 88 val lineNumber: Int, 89 val bounds: IntRect, 90 val location: SourceLocation?, 91 val children: List<ViewInfo>, 92 val layoutInfo: Any?, 93 val name: String? 94 ) { hasBoundsnull95 fun hasBounds(): Boolean = bounds.bottom != 0 && bounds.right != 0 96 97 fun allChildren(): List<ViewInfo> = children + children.flatMap { it.allChildren() } 98 toStringnull99 override fun toString(): String = 100 """($fileName:$lineNumber, 101 |bounds=(top=${bounds.top}, left=${bounds.left}, 102 |location=${location?.let { "(${it.offset}L${it.length}" } ?: "<none>"} 103 |bottom=${bounds.bottom}, right=${bounds.right}), 104 |childrenCount=${children.size})""" 105 .trimMargin() 106 } 107 108 /** 109 * View adapter that renders a `@Composable`. The `@Composable` is found by reading the 110 * `tools:composableName` attribute that contains the FQN. Additional attributes can be used to 111 * customize the behaviour of this view: 112 * - `tools:parameterProviderClass`: FQN of the [PreviewParameterProvider] to be instantiated by the 113 * [ComposeViewAdapter] that will be used as source for the `@Composable` parameters. 114 * - `tools:parameterProviderIndex`: The index within the [PreviewParameterProvider] of the value to 115 * be used in this particular instance. 116 * - `tools:paintBounds`: If true, the component boundaries will be painted. This is only meant for 117 * debugging purposes. 118 * - `tools:printViewInfos`: If true, the [ComposeViewAdapter] will log the tree of [ViewInfo] to 119 * logcat for debugging. 120 * - `tools:animationClockStartTime`: When set, a [PreviewAnimationClock] will control the 121 * animations in the [ComposeViewAdapter] context. 122 */ 123 @Suppress("unused") 124 @OptIn(UiToolingDataApi::class) 125 internal class ComposeViewAdapter : FrameLayout { 126 private val TAG = "ComposeViewAdapter" 127 128 /** [ComposeView] that will contain the [Composable] to preview. */ 129 private val composeView = ComposeView(context) 130 131 /** 132 * When enabled, generate and cache [ViewInfo] tree that can be inspected by the Preview to map 133 * components to source code. 134 */ 135 private var debugViewInfos = false 136 137 /** When enabled, paint the boundaries generated by layout nodes. */ 138 private var debugPaintBounds = false 139 internal var viewInfos: List<ViewInfo> = emptyList() 140 internal var designInfoList: List<String> = emptyList() 141 private val slotTableRecord = CompositionDataRecord.create() 142 143 /** Simple function name of the Composable being previewed. */ 144 private var composableName = "" 145 146 /** Whether the current Composable has animations. */ 147 private var hasAnimations = false 148 149 /** 150 * Saved exception from the last composition. Since we can not handle the exception during the 151 * composition, we save it and throw it during onLayout, this allows Studio to catch it and 152 * display it to the user. 153 */ 154 private val delayedException = ThreadSafeException() 155 156 /** 157 * The [Composable] to be rendered in the preview. It is initialized when this adapter is 158 * initialized. 159 */ 160 private var previewComposition: @Composable () -> Unit = {} 161 162 /** 163 * When true, the adapter will try to look objects that support the call [DESIGN_INFO_METHOD] 164 * within the slot table and populate [designInfoList]. Used to support rendering in Studio. 165 */ 166 private var lookForDesignInfoProviders = false 167 168 /** 169 * An additional [String] argument that will be passed to objects that support the 170 * [DESIGN_INFO_METHOD] call. Meant to be used by studio to as a way to request additional 171 * information from the Preview. 172 */ 173 private var designInfoProvidersArgument: String = "" 174 175 /** Callback invoked when onDraw has been called. */ 176 private var onDraw = {} 177 178 internal var stitchTrees = true 179 180 private val debugBoundsPaint = 181 Paint().apply { 182 pathEffect = DashPathEffect(floatArrayOf(5f, 10f, 15f, 20f), 0f) 183 style = Paint.Style.STROKE 184 color = Color.Red.toArgb() 185 } 186 187 private var composition: Composition? = null 188 189 constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { 190 init(attrs) 191 } 192 193 constructor( 194 context: Context, 195 attrs: AttributeSet, 196 defStyleAttr: Int 197 ) : super(context, attrs, defStyleAttr) { 198 init(attrs) 199 } 200 201 private val Group.fileName: String 202 get() = location?.sourceFile ?: "" 203 204 private val Group.lineNumber: Int 205 get() = location?.lineNumber ?: -1 206 207 /** Returns true if this [Group] has no source position information */ 208 private fun Group.hasNullSourcePosition(): Boolean = fileName.isEmpty() && lineNumber == -1 209 210 /** Returns true if this [Group] has no source position information and no children */ 211 private fun Group.isNullGroup(): Boolean = 212 hasNullSourcePosition() && 213 children.isEmpty() && 214 ((this as? NodeGroup)?.node as? LayoutInfo) == null 215 216 private fun Group.toViewInfo(): ViewInfo { 217 val layoutInfo = ((this as? NodeGroup)?.node as? LayoutInfo) 218 219 if (children.size == 1 && hasNullSourcePosition() && layoutInfo == null) { 220 // There is no useful information in this intermediate node, remove. 221 return children.single().toViewInfo() 222 } 223 224 val childrenViewInfo = children.filter { !it.isNullGroup() }.map { it.toViewInfo() } 225 226 // TODO: Use group names instead of indexing once it's supported 227 return ViewInfo( 228 location?.sourceFile ?: "", 229 location?.lineNumber ?: -1, 230 box, 231 location, 232 childrenViewInfo, 233 layoutInfo, 234 name 235 ) 236 } 237 238 /** Processes the recorded slot table and re-generates the [viewInfos] attribute. */ 239 private fun processViewInfos() { 240 val newViewInfos = slotTableRecord.store.map { it.asTree().toViewInfo() }.toList() 241 242 viewInfos = if (stitchTrees) stitchTrees(newViewInfos) else newViewInfos 243 244 if (debugViewInfos) { 245 val debugString = viewInfos.toDebugString() 246 Log.d(TAG, debugString) 247 } 248 } 249 250 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 251 super.onLayout(changed, left, top, right, bottom) 252 253 // If there was a pending exception then throw it here since Studio will catch it and show 254 // it to the user. 255 delayedException.throwIfPresent() 256 257 processViewInfos() 258 if (composableName.isNotEmpty()) { 259 findAndTrackAnimations() 260 if (lookForDesignInfoProviders) { 261 findDesignInfoProviders() 262 } 263 } 264 } 265 266 override fun onAttachedToWindow() { 267 composeView.rootView.setViewTreeLifecycleOwner(FakeSavedStateRegistryOwner) 268 super.onAttachedToWindow() 269 } 270 271 /** 272 * Finds all animations defined in the Compose tree where the root is the `@Composable` being 273 * previewed. 274 */ 275 private fun findAndTrackAnimations() { 276 val slotTrees = slotTableRecord.store.map { it.asTree() } 277 val isAnimationPreview = ::clock.isInitialized 278 AnimationSearch(::clock, ::requestLayout).let { 279 hasAnimations = it.searchAny(slotTrees) 280 if (isAnimationPreview && hasAnimations) { 281 it.attachAllAnimations(slotTrees) 282 } 283 } 284 } 285 286 /** 287 * Find all data objects within the slotTree that can invoke '[DESIGN_INFO_METHOD]', and store 288 * their result in [designInfoList]. 289 */ 290 private fun findDesignInfoProviders() { 291 val slotTrees = slotTableRecord.store.map { it.asTree() } 292 293 designInfoList = 294 slotTrees.flatMap { rootGroup -> 295 rootGroup 296 .findAll { group -> 297 (group.name != REMEMBER && group.hasDesignInfo()) || 298 group.children.any { child -> 299 child.name == REMEMBER && child.hasDesignInfo() 300 } 301 } 302 .mapNotNull { group -> 303 // Get the DesignInfoProviders from the group or one of its children 304 group.getDesignInfoOrNull(group.box) 305 ?: group.children.firstNotNullOfOrNull { 306 it.getDesignInfoOrNull(group.box) 307 } 308 } 309 } 310 } 311 312 private fun Group.hasDesignInfo(): Boolean = 313 data.any { it?.getDesignInfoMethodOrNull() != null } 314 315 private fun Group.getDesignInfoOrNull(box: IntRect): String? = 316 data.firstNotNullOfOrNull { it?.invokeGetDesignInfo(box.left, box.right) } 317 318 /** 319 * Check if the object supports the method call for [DESIGN_INFO_METHOD], which is expected to 320 * take two Integer arguments for coordinates and a String for additional encoded arguments that 321 * may be provided from Studio. 322 */ 323 private fun Any.getDesignInfoMethodOrNull(): Method? { 324 return try { 325 javaClass.getDeclaredMethod( 326 DESIGN_INFO_METHOD, 327 Integer.TYPE, 328 Integer.TYPE, 329 String::class.java 330 ) 331 } catch (e: NoSuchMethodException) { 332 null 333 } 334 } 335 336 @Suppress("BanUncheckedReflection") 337 private fun Any.invokeGetDesignInfo(x: Int, y: Int): String? { 338 return this.getDesignInfoMethodOrNull()?.let { designInfoMethod -> 339 try { 340 // Workaround for unchecked Method.invoke 341 val result = designInfoMethod.invoke(this, x, y, designInfoProvidersArgument) 342 (result as String).ifEmpty { null } 343 } catch (e: Exception) { 344 null 345 } 346 } 347 } 348 349 override fun dispatchDraw(canvas: Canvas) { 350 super.dispatchDraw(canvas) 351 352 onDraw() 353 if (!debugPaintBounds) { 354 return 355 } 356 357 viewInfos 358 .flatMap { listOf(it) + it.allChildren() } 359 .forEach { 360 if (it.hasBounds()) { 361 canvas.apply { 362 val pxBounds = 363 android.graphics.Rect( 364 it.bounds.left, 365 it.bounds.top, 366 it.bounds.right, 367 it.bounds.bottom 368 ) 369 drawRect(pxBounds, debugBoundsPaint) 370 } 371 } 372 } 373 } 374 375 /** Clock that controls the animations defined in the context of this [ComposeViewAdapter]. */ 376 @VisibleForTesting internal lateinit var clock: PreviewAnimationClock 377 378 /** Wraps a given [Preview] method an does any necessary setup. */ 379 @Composable 380 private fun WrapPreview(content: @Composable () -> Unit) { 381 // We need to replace the FontResourceLoader to avoid using ResourcesCompat. 382 // ResourcesCompat can not load fonts within Layoutlib and, since Layoutlib always runs 383 // the latest version, we do not need it. 384 @Suppress("DEPRECATION") 385 CompositionLocalProvider( 386 LocalFontLoader provides LayoutlibFontResourceLoader(context), 387 LocalFontFamilyResolver provides createFontFamilyResolver(context), 388 LocalOnBackPressedDispatcherOwner provides FakeOnBackPressedDispatcherOwner, 389 LocalActivityResultRegistryOwner provides FakeActivityResultRegistryOwner, 390 ) { 391 Inspectable(slotTableRecord, content) 392 } 393 } 394 395 /** 396 * Initializes the adapter and populates it with the given [Preview] composable. 397 * 398 * @param className name of the class containing the preview function 399 * @param methodName `@Preview` method name 400 * @param parameterProvider [Class] for the [PreviewParameterProvider] to be used as parameter 401 * input for this call. If null, no parameters will be passed to the composable. 402 * @param parameterProviderIndex when [parameterProvider] is not null, this index will reference 403 * the element in the [Sequence] to be used as parameter. 404 * @param debugPaintBounds if true, the view will paint the boundaries around the layout 405 * elements. 406 * @param debugViewInfos if true, it will generate the [ViewInfo] structures and will log it. 407 * @param animationClockStartTime if positive, [clock] will be defined and will control the 408 * animations defined in the context of the `@Composable` being previewed. 409 * @param lookForDesignInfoProviders if true, it will try to populate [designInfoList]. 410 * @param designInfoProvidersArgument String to use as an argument when populating 411 * [designInfoList]. 412 * @param onCommit callback invoked after every commit of the preview composable. 413 * @param onDraw callback invoked after every draw of the adapter. Only for test use. 414 */ 415 @Suppress("DEPRECATION") 416 @OptIn(ExperimentalComposeUiApi::class) 417 @VisibleForTesting 418 internal fun init( 419 className: String, 420 methodName: String, 421 parameterProvider: Class<out PreviewParameterProvider<*>>? = null, 422 parameterProviderIndex: Int = 0, 423 debugPaintBounds: Boolean = false, 424 debugViewInfos: Boolean = false, 425 animationClockStartTime: Long = -1, 426 lookForDesignInfoProviders: Boolean = false, 427 designInfoProvidersArgument: String? = null, 428 onCommit: () -> Unit = {}, 429 onDraw: () -> Unit = {} 430 ) { 431 this.debugPaintBounds = debugPaintBounds 432 this.debugViewInfos = debugViewInfos 433 this.composableName = methodName 434 this.lookForDesignInfoProviders = lookForDesignInfoProviders 435 this.designInfoProvidersArgument = designInfoProvidersArgument ?: "" 436 this.onDraw = onDraw 437 438 previewComposition = 439 @Composable { 440 SideEffect(onCommit) 441 442 WrapPreview { 443 val composer = currentComposer 444 // We need to delay the reflection instantiation of the class until we are in 445 // the 446 // composable to ensure all the right initialization has happened and the 447 // Composable 448 // class loads correctly. 449 val composable = { 450 try { 451 ComposableInvoker.invokeComposable( 452 className, 453 methodName, 454 composer, 455 *getPreviewProviderParameters( 456 parameterProvider, 457 parameterProviderIndex 458 ) 459 ) 460 } catch (t: Throwable) { 461 // If there is an exception, store it for later but do not catch it so 462 // compose can handle it and dispose correctly. 463 var exception: Throwable = t 464 // Find the root cause and use that for the delayedException. 465 while (exception is ReflectiveOperationException) { 466 exception = exception.cause ?: break 467 } 468 delayedException.set(exception) 469 throw t 470 } 471 } 472 if (animationClockStartTime >= 0) { 473 // When animation inspection is enabled, i.e. when a valid (non-negative) 474 // `animationClockStartTime` is passed, set the Preview Animation Clock. 475 // This 476 // clock will control the animations defined in this `ComposeViewAdapter` 477 // from Android Studio. 478 clock = PreviewAnimationClock { 479 // Invalidate the descendants of this ComposeViewAdapter's only 480 // grandchild 481 // (an AndroidOwner) when setting the clock time to make sure the 482 // Compose 483 // Preview will animate when the states are read inside the draw scope. 484 val composeView = getChildAt(0) as ComposeView 485 (composeView.getChildAt(0) as? ViewRootForTest)?.invalidateDescendants() 486 // Send pending apply notifications to ensure the animation duration 487 // will 488 // be read in the correct frame. 489 Snapshot.sendApplyNotifications() 490 } 491 } 492 composable() 493 } 494 } 495 composeView.setContent(previewComposition) 496 invalidate() 497 } 498 499 /** Disposes the Compose elements allocated during [init] */ 500 internal fun dispose() { 501 composeView.disposeComposition() 502 if (::clock.isInitialized) { 503 clock.dispose() 504 } 505 FakeSavedStateRegistryOwner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED 506 FakeViewModelStoreOwner.viewModelStore.clear() 507 } 508 509 /** 510 * Returns whether this `@Composable` has animations. This allows Android Studio to decide if 511 * the Animation Inspector icon should be displayed for this preview. The reason for using a 512 * method instead of the property directly is we use Java reflection to call it from Android 513 * Studio, and to find the property we'd need to filter the method names using `contains` 514 * instead of `equals`. 515 */ 516 fun hasAnimations() = hasAnimations 517 518 private fun init(attrs: AttributeSet) { 519 // ComposeView and lifecycle initialization 520 setViewTreeLifecycleOwner(FakeSavedStateRegistryOwner) 521 setViewTreeSavedStateRegistryOwner(FakeSavedStateRegistryOwner) 522 setViewTreeViewModelStoreOwner(FakeViewModelStoreOwner) 523 addView(composeView) 524 525 val composableName = attrs.getAttributeValue(TOOLS_NS_URI, "composableName") ?: return 526 val className = composableName.substringBeforeLast('.') 527 val methodName = composableName.substringAfterLast('.') 528 val parameterProviderIndex = 529 attrs.getAttributeIntValue(TOOLS_NS_URI, "parameterProviderIndex", 0) 530 val parameterProviderClass = 531 attrs 532 .getAttributeValue(TOOLS_NS_URI, "parameterProviderClass") 533 ?.asPreviewProviderClass() 534 535 val animationClockStartTime = 536 try { 537 attrs.getAttributeValue(TOOLS_NS_URI, "animationClockStartTime").toLong() 538 } catch (e: Exception) { 539 -1L 540 } 541 542 init( 543 className = className, 544 methodName = methodName, 545 parameterProvider = parameterProviderClass, 546 parameterProviderIndex = parameterProviderIndex, 547 debugPaintBounds = 548 attrs.getAttributeBooleanValue(TOOLS_NS_URI, "paintBounds", debugPaintBounds), 549 debugViewInfos = 550 attrs.getAttributeBooleanValue(TOOLS_NS_URI, "printViewInfos", debugViewInfos), 551 animationClockStartTime = animationClockStartTime, 552 lookForDesignInfoProviders = 553 attrs.getAttributeBooleanValue( 554 TOOLS_NS_URI, 555 "findDesignInfoProviders", 556 lookForDesignInfoProviders 557 ), 558 designInfoProvidersArgument = 559 attrs.getAttributeValue(TOOLS_NS_URI, "designInfoProvidersArgument") 560 ) 561 } 562 563 @SuppressLint("VisibleForTests") 564 private val FakeSavedStateRegistryOwner = 565 object : SavedStateRegistryOwner { 566 val lifecycleRegistry = LifecycleRegistry.createUnsafe(this) 567 private val controller = 568 SavedStateRegistryController.create(this).apply { performRestore(Bundle()) } 569 570 init { 571 lifecycleRegistry.currentState = Lifecycle.State.RESUMED 572 } 573 574 override val savedStateRegistry: SavedStateRegistry 575 get() = controller.savedStateRegistry 576 577 override val lifecycle: LifecycleRegistry 578 get() = lifecycleRegistry 579 } 580 581 private val FakeViewModelStoreOwner = 582 object : ViewModelStoreOwner { 583 private val vmStore = ViewModelStore() 584 585 override val viewModelStore = vmStore 586 } 587 588 private val FakeOnBackPressedDispatcherOwner = 589 object : OnBackPressedDispatcherOwner { 590 override val onBackPressedDispatcher = OnBackPressedDispatcher() 591 592 override val lifecycle: LifecycleRegistry 593 get() = FakeSavedStateRegistryOwner.lifecycleRegistry 594 } 595 596 private val FakeActivityResultRegistryOwner = 597 object : ActivityResultRegistryOwner { 598 override val activityResultRegistry = 599 object : ActivityResultRegistry() { 600 override fun <I : Any?, O : Any?> onLaunch( 601 requestCode: Int, 602 contract: ActivityResultContract<I, O>, 603 input: I, 604 options: ActivityOptionsCompat? 605 ) { 606 throw IllegalStateException("Calling launch() is not supported in Preview") 607 } 608 } 609 } 610 } 611