[go: nahoru, domu]

blob: 0cd20c0c3761a562c00b7b11cd850fa358a0a938 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.test.android
import android.R
import android.annotation.TargetApi
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Picture
import android.graphics.RenderNode
import android.os.Build
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.compose.Compose
import androidx.compose.Composition
import androidx.compose.FrameManager
import androidx.compose.Recomposer
import androidx.compose.frames.currentFrame
import androidx.compose.frames.inFrame
import androidx.ui.core.AndroidComposeView
import androidx.ui.core.ComponentNode
import androidx.ui.core.DrawNode
import androidx.ui.core.setContent
import androidx.ui.test.ComposeBenchmarkScope
import androidx.ui.test.ComposeTestCase
import androidx.ui.test.setupContent
/**
* Factory method to provide implementation of [ComposeBenchmarkScope].
*/
fun <T : ComposeTestCase> createAndroidComposeBenchmarkRunner(
testCaseFactory: () -> T,
activity: Activity
): ComposeBenchmarkScope<T> {
return AndroidComposeTestCaseRunner(testCaseFactory, activity)
}
internal class AndroidComposeTestCaseRunner<T : ComposeTestCase>(
private val testCaseFactory: () -> T,
private val activity: Activity
) : ComposeBenchmarkScope<T> {
override val measuredWidth: Int
get() = view!!.measuredWidth
override val measuredHeight: Int
get() = view!!.measuredHeight
internal var view: ViewGroup? = null
private set
private var composition: Composition? = null
override var didLastRecomposeHaveChanges = false
private set
private val supportsRenderNode = Build.VERSION.SDK_INT >= 29
private val screenWithSpec: Int
private val screenHeightSpec: Int
private val capture = if (supportsRenderNode) RenderNodeCapture() else PictureCapture()
private var canvas: Canvas? = null
private var recomposer: Recomposer? = null
private var simulationState: SimulationState = SimulationState.Initialized
private var testCase: T? = null
init {
val displayMetrics = DisplayMetrics()
activity.windowManager.defaultDisplay.getMetrics(displayMetrics)
val height = displayMetrics.heightPixels
val width = displayMetrics.widthPixels
screenWithSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST)
screenHeightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST)
}
override fun createTestCase() {
require(view == null) { "Content was already set!" }
require(testCase == null) { "Content was already set!" }
testCase = testCaseFactory()
simulationState = SimulationState.TestCaseCreated
}
override fun emitContent() {
require(view == null) { "Content was already set!" }
require(testCase != null && simulationState == SimulationState.TestCaseCreated) {
"Need to call onPreEmitContent before emitContent!"
}
recomposer = Recomposer.current()
composition = activity.setContent { testCase!!.emitContent() }
FrameManager.nextFrame()
view = findComposeView(activity)!!
simulationState = SimulationState.EmitContentDone
}
override fun hasPendingChanges(): Boolean {
if (Recomposer.hasPendingChanges() || hasPendingChangesInFrame()) {
FrameManager.nextFrame()
}
return Recomposer.hasPendingChanges()
}
/**
* The reason we have this method is that if a model gets changed in the same frame as created
* it won'd trigger pending frame. So [Recompose#hasPendingChanges] stays false. Committing
* the current frame does not help either. So we need to check this in order to know if we
* need to recompose.
*/
private fun hasPendingChangesInFrame(): Boolean {
return inFrame && currentFrame().hasPendingChanges()
}
override fun measure() {
getView().measure(screenWithSpec, screenHeightSpec)
simulationState = SimulationState.MeasureDone
}
override fun measureWithSpec(widthSpec: Int, heightSpec: Int) {
getView().measure(widthSpec, heightSpec)
simulationState = SimulationState.MeasureDone
}
override fun drawPrepare() {
require(simulationState == SimulationState.LayoutDone ||
simulationState == SimulationState.DrawDone) {
"Draw can be only executed after layout or draw, current state is '$simulationState'"
}
canvas = capture.beginRecording(getView().width, getView().height)
simulationState = SimulationState.DrawPrepared
}
override fun draw() {
require(simulationState == SimulationState.DrawPrepared) {
"You need to call 'drawPrepare' before calling 'draw'."
}
getView().draw(canvas)
simulationState = SimulationState.DrawInProgress
}
override fun drawFinish() {
require(simulationState == SimulationState.DrawInProgress) {
"You need to call 'draw' before calling 'drawFinish'."
}
capture.endRecording()
simulationState = SimulationState.DrawDone
}
override fun drawToBitmap() {
drawPrepare()
draw()
drawFinish()
}
override fun requestLayout() {
getView().requestLayout()
}
override fun layout() {
require(simulationState == SimulationState.MeasureDone) {
"Layout can be only executed after measure, current state is '$simulationState'"
}
val view = getView()
view.layout(view.left, view.top, view.right, view.bottom)
simulationState = SimulationState.LayoutDone
}
override fun recompose() {
if (hasPendingChanges()) {
didLastRecomposeHaveChanges = true
recomposer!!.recomposeSync()
} else {
didLastRecomposeHaveChanges = false
}
simulationState = SimulationState.RecomposeDone
}
override fun doFrame() {
if (view == null) {
setupContent()
}
recompose()
measure()
layout()
drawToBitmap()
}
override fun invalidateViews() {
invalidateViews(getView())
}
override fun disposeContent() {
if (view == null) {
// Already disposed or never created any content
return
}
// TODO(pavlis): replace with activity.disposeComposition() after it gets fixed.
Compose.disposeComposition((view as AndroidComposeView).root, activity, null)
// Clear the view
val rootView = activity.findViewById(R.id.content) as ViewGroup
rootView.removeAllViews()
// Important so we can set the content again.
view = null
testCase = null
simulationState = SimulationState.Initialized
}
override fun capturePreviewPictureToActivity() {
require(measuredWidth > 0 && measuredHeight > 0) {
"Preview can't be used on empty view. Did you run measure & layout before calling it?"
}
val picture = Picture()
val canvas = picture.beginRecording(getView().measuredWidth, getView().measuredHeight)
getView().draw(canvas)
picture.endRecording()
val imageView = ImageView(activity)
val bitmap: Bitmap
if (Build.VERSION.SDK_INT >= 28) {
bitmap = Bitmap.createBitmap(picture)
} else {
val width = picture.width.coerceAtLeast(1)
val height = picture.height.coerceAtLeast(1)
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
Canvas(bitmap).drawPicture(picture)
}
imageView.setImageBitmap(bitmap)
activity.setContentView(imageView)
}
private fun getView(): ViewGroup {
require(view != null) { "View was not set! Call setupContent first!" }
return view!!
}
override fun getTestCase(): T {
return testCase!!
}
}
private enum class SimulationState {
Initialized,
TestCaseCreated,
EmitContentDone,
MeasureDone,
LayoutDone,
DrawPrepared,
DrawInProgress,
DrawDone,
RecomposeDone
}
private fun findComposeView(activity: Activity): AndroidComposeView? {
return findComposeView(activity.findViewById(android.R.id.content) as ViewGroup)
}
private fun findComposeView(view: View): AndroidComposeView? {
if (view is AndroidComposeView) {
return view
}
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val composeView = findComposeView(view.getChildAt(i))
if (composeView != null) {
return composeView
}
}
}
return null
}
private fun invalidateViews(view: View) {
view.invalidate()
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
invalidateViews(child)
}
}
if (view is AndroidComposeView) {
invalidateComponentNodes(view.root)
}
}
private fun invalidateComponentNodes(node: ComponentNode) {
if (node is DrawNode) {
node.invalidate()
}
node.visitChildren { child ->
invalidateComponentNodes(child)
}
}
// We must separate the use of RenderNode so that it isn't referenced in any
// way on platforms that don't have it. This extracts RenderNode use to a
// potentially unloaded class, RenderNodeCapture.
private interface DrawCapture {
fun beginRecording(width: Int, height: Int): Canvas
fun endRecording()
}
@TargetApi(Build.VERSION_CODES.Q)
private class RenderNodeCapture : DrawCapture {
private val renderNode = RenderNode("Test")
override fun beginRecording(width: Int, height: Int): Canvas {
renderNode.setPosition(0, 0, width, height)
return renderNode.beginRecording()
}
override fun endRecording() {
renderNode.endRecording()
}
}
private class PictureCapture : DrawCapture {
private val picture = Picture()
override fun beginRecording(width: Int, height: Int): Canvas {
return picture.beginRecording(width, height)
}
override fun endRecording() {
picture.endRecording()
}
}