android: [render-validation] collection of adjustments (#9898)
- A triangle tracker under the progress bar to track the test
currently in view within the scroll view.
- Text label to indicate the current device under test when a
test is running.
- add filter intent arg
- Add Fox (animation, skinning) and TransmissionRoughnessTest
(transmission) to the test.
- Add applyAnimation to the ValidationRunner
- Make camera zoom in for default test and adjust tolerance
acoordingly
- Fix non-determinism of SSR by
- waiting for more frames
- account for false returned from beginFrame
- Clear frame history on new test entry
- Include Build.HARDWARE information into result
- Make results array more thread-safe
- Remove unused paths
- Adjust tolerance for two tests
This commit is contained in:
@@ -316,9 +316,9 @@ class ModelViewer(
|
||||
* @param frameTimeNanos time in nanoseconds when the frame started being rendered,
|
||||
* typically comes from {@link android.view.Choreographer.FrameCallback}
|
||||
*/
|
||||
fun render(frameTimeNanos: Long) {
|
||||
fun render(frameTimeNanos: Long): Boolean {
|
||||
if (!uiHelper.isReadyToRender) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// Allow the resource loader to finalize textures that have become ready.
|
||||
@@ -337,7 +337,9 @@ class ModelViewer(
|
||||
}
|
||||
|
||||
// Render the scene, unless the renderer wants to skip the frame.
|
||||
var rendered = false
|
||||
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) {
|
||||
rendered = true
|
||||
renderer.render(view)
|
||||
|
||||
debugFrameCallback?.let {
|
||||
@@ -360,6 +362,7 @@ class ModelViewer(
|
||||
|
||||
renderer.endFrame()
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -16,14 +16,15 @@ filament {
|
||||
iblOutputDir = project.layout.projectDirectory.dir("src/main/assets/envs")
|
||||
}
|
||||
|
||||
// don't forget to update MainACtivity.kt when/if changing this.
|
||||
tasks.register('copyDamagedHelmetGltf', Copy) {
|
||||
// don't forget to update MainActivity.kt when/if changing this.
|
||||
tasks.register('copyModels', Copy) {
|
||||
from file("../../../third_party/models/DamagedHelmet/DamagedHelmet.glb")
|
||||
from file("../../../third_party/models/Fox/Fox.glb")
|
||||
from file("../../../third_party/models/TransmissionRoughnessTest/TransmissionRoughnessTest.glb")
|
||||
into file("src/main/assets/models")
|
||||
rename {String fileName -> "helmet.glb"}
|
||||
}
|
||||
|
||||
preBuild.dependsOn copyDamagedHelmetGltf
|
||||
preBuild.dependsOn copyModels
|
||||
|
||||
clean.doFirst {
|
||||
delete "src/main/assets/envs"
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
"vulkan"
|
||||
],
|
||||
"models": {
|
||||
"DamagedHelmet": "helmet.glb"
|
||||
"DamagedHelmet": "DamagedHelmet.glb",
|
||||
"Fox": "Fox.glb",
|
||||
"TransmissionRoughnessTest": "TransmissionRoughnessTest.glb"
|
||||
},
|
||||
"presets": [
|
||||
{
|
||||
@@ -20,7 +22,7 @@
|
||||
"center": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
-0.8
|
||||
],
|
||||
"lookAt": [
|
||||
0,
|
||||
@@ -81,7 +83,7 @@
|
||||
"tolerance": {
|
||||
"maxAbsDiff": 0.09,
|
||||
"shiftRadius": 2,
|
||||
"blurRadius": 3,
|
||||
"blurRadius": 4,
|
||||
"maxFailingPixelsFraction": 0.0005
|
||||
},
|
||||
"rendering": {}
|
||||
@@ -201,7 +203,8 @@
|
||||
{
|
||||
"name": "ssao_bent_normals",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"relaxed_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.ssao.enabled": true,
|
||||
@@ -442,7 +445,8 @@
|
||||
{
|
||||
"name": "shadow_vsm_highPrecision",
|
||||
"apply_presets": [
|
||||
"base"
|
||||
"base",
|
||||
"relaxed_tolerance"
|
||||
],
|
||||
"rendering": {
|
||||
"view.shadowType": "VSM",
|
||||
@@ -574,6 +578,36 @@
|
||||
"view.antiAliasing": "FXAA",
|
||||
"view.dof.enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fox_animation",
|
||||
"models": [
|
||||
"Fox"
|
||||
],
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"animation": {
|
||||
"enabled": true,
|
||||
"time": 0.5
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "transmission",
|
||||
"models": [
|
||||
"TransmissionRoughnessTest"
|
||||
],
|
||||
"apply_presets": [
|
||||
"base"
|
||||
],
|
||||
"rendering": {
|
||||
"animation": {
|
||||
"enabled": true,
|
||||
"time": 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -31,8 +31,10 @@ import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import com.google.android.filament.utils.KTX1Loader
|
||||
@@ -62,8 +64,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
private lateinit var choreographer: Choreographer
|
||||
private lateinit var statusTextView: TextView
|
||||
private lateinit var testResultsHeader: TextView
|
||||
private lateinit var progressContainer: FrameLayout
|
||||
private lateinit var testProgress: com.google.android.filament.validation.TestProgressBar
|
||||
private lateinit var progressTriangle: ImageView
|
||||
private lateinit var scrollView: ScrollView
|
||||
private lateinit var testSummaryText: TextView
|
||||
private lateinit var deviceInfoText: TextView
|
||||
private lateinit var resultsContainer: LinearLayout
|
||||
private lateinit var inputManager: ValidationInputManager
|
||||
private var currentInput: ValidationInputManager.ValidationInput? = null
|
||||
@@ -100,8 +106,10 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
private val frameScheduler = object : Choreographer.FrameCallback {
|
||||
override fun doFrame(frameTimeNanos: Long) {
|
||||
choreographer.postFrameCallback(this)
|
||||
modelViewer?.render(frameTimeNanos)
|
||||
validationRunner?.onFrame(frameTimeNanos)
|
||||
val rendered = modelViewer?.render(frameTimeNanos) ?: false
|
||||
if (rendered) {
|
||||
validationRunner?.onFrame(frameTimeNanos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +123,22 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
|
||||
statusTextView = findViewById(R.id.status_text)
|
||||
testResultsHeader = findViewById(R.id.test_results_header)
|
||||
progressContainer = findViewById(R.id.progress_container)
|
||||
testProgress = findViewById(R.id.test_progress)
|
||||
progressTriangle = findViewById(R.id.progress_triangle)
|
||||
scrollView = findViewById(R.id.scroll_view)
|
||||
testSummaryText = findViewById(R.id.test_summary_text)
|
||||
deviceInfoText = findViewById(R.id.device_info_text)
|
||||
deviceInfoText.text = "Running on: ${android.os.Build.MODEL}"
|
||||
resultsContainer = findViewById(R.id.results_container)
|
||||
|
||||
scrollView.viewTreeObserver.addOnScrollChangedListener {
|
||||
updateTrianglePosition()
|
||||
}
|
||||
scrollView.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
updateTrianglePosition()
|
||||
}
|
||||
|
||||
runButton = findViewById(R.id.run_button)
|
||||
loadButton = findViewById(R.id.load_button)
|
||||
optionsButton = findViewById(R.id.options_button)
|
||||
@@ -438,6 +458,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
return ValidationResultManager(
|
||||
outputDir = outputDir,
|
||||
deviceName = android.os.Build.MODEL,
|
||||
deviceHardware = android.os.Build.HARDWARE,
|
||||
deviceCodeName = android.os.Build.DEVICE,
|
||||
androidVersion = android.os.Build.VERSION.RELEASE,
|
||||
androidBuildNumber = android.os.Build.DISPLAY
|
||||
@@ -470,7 +491,9 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
|
||||
|
||||
testProgress.visibility = View.VISIBLE
|
||||
progressContainer.visibility = View.VISIBLE
|
||||
testSummaryText.visibility = View.GONE
|
||||
deviceInfoText.visibility = View.VISIBLE
|
||||
totalPassed = 0
|
||||
totalFailed = 0
|
||||
testProgress.reset(1)
|
||||
@@ -493,6 +516,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
validationRunner = ValidationRunner(this, surfaceView, config, resultManager!!, backendFilter)
|
||||
validationRunner?.callback = this
|
||||
validationRunner?.generateGoldens = input.generateGoldens
|
||||
validationRunner?.testFilter = input.testFilter
|
||||
validationRunner?.start()
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -540,6 +564,36 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
private var currentDiffBitmap: Bitmap? = null
|
||||
private var totalPassed = 0
|
||||
private var totalFailed = 0
|
||||
private var totalTestsCount = 1
|
||||
|
||||
private fun updateTrianglePosition() {
|
||||
if (progressContainer.visibility != View.VISIBLE || resultsContainer.childCount == 0) return
|
||||
|
||||
val scrollY = scrollView.scrollY
|
||||
val visibleHeight = scrollView.height
|
||||
val centerY = scrollY + visibleHeight / 2f
|
||||
|
||||
var bestIndex = -1
|
||||
var minDistance = Float.MAX_VALUE
|
||||
for (i in 0 until resultsContainer.childCount) {
|
||||
val child = resultsContainer.getChildAt(i)
|
||||
val childCenterY = child.top + child.height / 2f
|
||||
val dist = Math.abs(childCenterY - centerY)
|
||||
if (dist < minDistance) {
|
||||
minDistance = dist
|
||||
bestIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex >= 0) {
|
||||
val progressWidth = testProgress.width
|
||||
if (progressWidth > 0 && totalTestsCount > 0) {
|
||||
val segmentWidth = progressWidth.toFloat() / totalTestsCount
|
||||
val targetX = (bestIndex * segmentWidth) + (segmentWidth / 2f)
|
||||
progressTriangle.translationX = targetX - progressTriangle.width / 2f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTestFinished(result: ValidationResult) {
|
||||
runOnUiThread {
|
||||
@@ -640,6 +694,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
|
||||
resultContainer.addView(imagesRow)
|
||||
resultsContainer.addView(resultContainer)
|
||||
resultsContainer.post { updateTrianglePosition() }
|
||||
|
||||
// Clear current images for next test
|
||||
currentRenderedBitmap = null
|
||||
@@ -651,7 +706,9 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
|
||||
override fun onTestProgress(current: Int, total: Int) {
|
||||
runOnUiThread {
|
||||
totalTestsCount = Math.max(1, total)
|
||||
testProgress.setMax(total)
|
||||
updateTrianglePosition()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,6 +720,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
|
||||
val html = "Passed: <font color='$colorPassed'><b>$totalPassed</b></font> / $total Failed: <font color='$colorFailed'><b>$totalFailed</b></font>"
|
||||
testSummaryText.text = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
|
||||
testSummaryText.visibility = View.VISIBLE
|
||||
deviceInfoText.visibility = View.GONE
|
||||
statusTextView.text = "All tests finished!"
|
||||
|
||||
// Re-enable UI
|
||||
|
||||
@@ -37,7 +37,8 @@ class ValidationInputManager(private val context: Context) {
|
||||
val outputDir: File,
|
||||
val generateGoldens: Boolean,
|
||||
val autoRun: Boolean = false,
|
||||
val sourceZip: File? = null
|
||||
val sourceZip: File? = null,
|
||||
val testFilter: String? = null
|
||||
)
|
||||
|
||||
public fun getBaseDir() : File {
|
||||
@@ -71,6 +72,8 @@ class ValidationInputManager(private val context: Context) {
|
||||
val autoRun = intent.getBooleanExtra("auto_run", false) ||
|
||||
intent.getBooleanExtra("generate_goldens", false)
|
||||
|
||||
val testFilter = if (autoRun) intent.getStringExtra("test_filter") else null
|
||||
|
||||
val outputPath = intent.getStringExtra("output_path")
|
||||
|
||||
Log.i(TAG, "Resolving config with outputPath: $outputPath")
|
||||
@@ -134,7 +137,7 @@ class ValidationInputManager(private val context: Context) {
|
||||
else -> null
|
||||
}
|
||||
|
||||
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, sourceZipFile)
|
||||
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, sourceZipFile, testFilter)
|
||||
}
|
||||
|
||||
private var lastUnzippedFile: String? = null
|
||||
@@ -150,7 +153,8 @@ class ValidationInputManager(private val context: Context) {
|
||||
outputDir = outputDir,
|
||||
generateGoldens = false,
|
||||
autoRun = false,
|
||||
sourceZip = file
|
||||
sourceZip = file,
|
||||
testFilter = null
|
||||
)
|
||||
return newInput
|
||||
}
|
||||
@@ -227,14 +231,15 @@ class ValidationInputManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Copy DamagedHelmet.glb
|
||||
// Copy models
|
||||
val modelsDir = File(filesDir, "models")
|
||||
modelsDir.mkdirs()
|
||||
val modelOut = File(modelsDir, "helmet.glb")
|
||||
|
||||
assetManager.open("models/helmet.glb").use { input ->
|
||||
FileOutputStream(modelOut).use { output ->
|
||||
input.copyTo(output)
|
||||
assetManager.list("models")?.forEach { modelFileName ->
|
||||
val modelOut = File(modelsDir, modelFileName)
|
||||
assetManager.open("models/$modelFileName").use { input ->
|
||||
FileOutputStream(modelOut).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,9 +252,15 @@ class ValidationInputManager(private val context: Context) {
|
||||
|
||||
val models = configJson.getJSONObject("models")
|
||||
|
||||
// Ensure the default model points to the extracted file
|
||||
// We can use absolute path to be safe since we know where it is now.
|
||||
models.put("DamagedHelmet", modelOut.absolutePath)
|
||||
// Update all model paths to point to the extracted files in the models directory
|
||||
val keys = models.keys()
|
||||
val newModels = JSONObject()
|
||||
while (keys.hasNext()) {
|
||||
val key = keys.next()
|
||||
val fileName = models.getString(key)
|
||||
newModels.put(key, java.io.File(modelsDir, fileName).absolutePath)
|
||||
}
|
||||
configJson.put("models", newModels)
|
||||
|
||||
configOut.writeText(configJson.toString(2))
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ data class ValidationResult(
|
||||
class ValidationResultManager(
|
||||
private val outputDir: File,
|
||||
private val deviceName: String,
|
||||
private val deviceHardware: String,
|
||||
private val deviceCodeName: String,
|
||||
private val androidVersion: String,
|
||||
private val androidBuildNumber: String
|
||||
@@ -44,7 +45,7 @@ class ValidationResultManager(
|
||||
private const val TAG = "ValidationResultManager"
|
||||
}
|
||||
|
||||
private val results = mutableListOf<ValidationResult>()
|
||||
private val results = java.util.concurrent.CopyOnWriteArrayList<ValidationResult>()
|
||||
private val gpuDriverInfos = JSONObject()
|
||||
|
||||
init {
|
||||
@@ -293,6 +294,7 @@ class ValidationResultManager(
|
||||
metadataObject.put("gpu_driver_info", gpuDriverInfos)
|
||||
metadataObject.put("total_time_ms", totalTimeMs)
|
||||
metadataObject.put("device_name", deviceName ?: "")
|
||||
metadataObject.put("device_hardware", deviceHardware ?: "")
|
||||
metadataObject.put("device_code_name", deviceCodeName ?: "")
|
||||
metadataObject.put("android_version", androidVersion ?: "")
|
||||
metadataObject.put("android_build_number", androidBuildNumber ?: "")
|
||||
|
||||
@@ -65,7 +65,22 @@ class ValidationRunner(
|
||||
|
||||
// Sort by backend then model. This tries to minimize the number of times we need to
|
||||
// recreate the Engine, and then the ModelViewer.
|
||||
expanded.sortedWith(compareBy({ it.first.backend }, { it.second }))
|
||||
val sorted = expanded.sortedWith(compareBy({ it.first.backend }, { it.second }))
|
||||
|
||||
val filter = testFilter
|
||||
if (filter != null && filter.count { it == '.' } == 2) {
|
||||
val parts = filter.split(".")
|
||||
val nameRegex = ("^" + parts[0].replace("*", ".*") + "$").toRegex()
|
||||
val backendRegex = ("^" + parts[1].replace("*", ".*") + "$").toRegex()
|
||||
val modelRegex = ("^" + parts[2].replace("*", ".*") + "$").toRegex()
|
||||
|
||||
sorted.filter { (test, model) ->
|
||||
val testBackend = test.backend ?: "opengl"
|
||||
test.name.matches(nameRegex) && testBackend.matches(backendRegex) && model.matches(modelRegex)
|
||||
}
|
||||
} else {
|
||||
sorted
|
||||
}
|
||||
}
|
||||
|
||||
enum class State {
|
||||
@@ -86,6 +101,7 @@ class ValidationRunner(
|
||||
|
||||
var callback: Callback? = null
|
||||
var generateGoldens: Boolean = false
|
||||
var testFilter: String? = null
|
||||
|
||||
fun start() {
|
||||
if (sortedTests.isEmpty()) {
|
||||
@@ -215,11 +231,12 @@ class ValidationRunner(
|
||||
content.renderer = mv.renderer
|
||||
content.scene = mv.scene
|
||||
content.lightManager = mv.engine.lightManager
|
||||
|
||||
// Tick
|
||||
val deltaTime = 1.0f / 60.0f
|
||||
engine.tick(mv.engine, content, deltaTime)
|
||||
|
||||
applyAnimation(mv, currentTestConfig!!.rendering)
|
||||
|
||||
frameCounter++
|
||||
if (engine.shouldClose()) {
|
||||
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
|
||||
@@ -233,6 +250,30 @@ class ValidationRunner(
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyAnimation(mv: com.google.android.filament.utils.ModelViewer, rendering: JSONObject) {
|
||||
val animator = mv.animator ?: return
|
||||
if (animator.animationCount == 0) return
|
||||
|
||||
var enabled = false
|
||||
var time = 0.0f
|
||||
|
||||
if (rendering.has("animation")) {
|
||||
val anim = rendering.optJSONObject("animation")
|
||||
if (anim != null) {
|
||||
enabled = anim.optBoolean("enabled", false)
|
||||
time = anim.optDouble("time", 0.0).toFloat()
|
||||
}
|
||||
} else {
|
||||
enabled = rendering.optBoolean("animation.enabled", false)
|
||||
time = rendering.optDouble("animation.time", 0.0).toFloat()
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
animator.applyAnimation(0, time)
|
||||
animator.updateBoneMatrices()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAutomation() {
|
||||
val test = currentTestConfig!!
|
||||
val specJson = JSONObject()
|
||||
@@ -243,13 +284,19 @@ class ValidationRunner(
|
||||
currentEngine = AutomationEngine(fullSpec)
|
||||
val options = AutomationEngine.Options()
|
||||
options.sleepDuration = 0.0f // Minimal sleep, let frames drive it
|
||||
options.minFrameCount = 1 // Ensure some frames pass
|
||||
options.minFrameCount = 15 // Ensure enough frames pass for temporal accumulation
|
||||
currentEngine?.setOptions(options)
|
||||
|
||||
// Use batch mode to ensure shouldClose() works reliably
|
||||
currentEngine?.startBatchMode()
|
||||
currentEngine?.signalBatchMode() // Start immediately
|
||||
|
||||
// Clear the frame history so that temporal effects (like SSR or TAA) don't bleed over
|
||||
// or ghost from the visual output of the previous test.
|
||||
modelViewer?.let { mv ->
|
||||
mv.view.clearFrameHistory(mv.engine)
|
||||
}
|
||||
|
||||
frameCounter = 0
|
||||
currentState = State.RUNNING_TEST
|
||||
}
|
||||
@@ -297,8 +344,6 @@ class ValidationRunner(
|
||||
Log.i("ValidationRunner", "Found golden at ${goldenFile.absolutePath}")
|
||||
} else {
|
||||
Log.w("ValidationRunner", "Golden not found for $testFullName. Searched in: ${searchPaths.joinToString { it.absolutePath }}")
|
||||
// Fallback to old behavior for reference if everything else fails
|
||||
goldenFile = modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File(modelParent, "golden/${testFullName}.png")
|
||||
}
|
||||
|
||||
Thread {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:clipChildren="false"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingTop="20dp"
|
||||
@@ -168,13 +169,30 @@
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="12dp" />
|
||||
|
||||
<com.google.android.filament.validation.TestProgressBar
|
||||
android:id="@+id/test_progress"
|
||||
<FrameLayout
|
||||
android:id="@+id/progress_container"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:visibility="gone" />
|
||||
android:clipChildren="false"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.filament.validation.TestProgressBar
|
||||
android:id="@+id/test_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/progress_triangle"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="8dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginTop="4dp"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_triangle_up" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/test_summary_text"
|
||||
@@ -184,9 +202,18 @@
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/device_info_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/controls_container"
|
||||
|
||||
Reference in New Issue
Block a user