Skip to content
This repository was archived by the owner on Feb 5, 2021. It is now read-only.

Commit 66a9c80

Browse files
[DNM] Introduce ViewFactoryTransformer with vibrating demo effect.
1 parent de5171b commit 66a9c80

File tree

6 files changed

+245
-14
lines changed

6 files changed

+245
-14
lines changed

core-compose/src/main/java/com/squareup/workflow/ui/compose/ComposeViewFactory.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ inline fun <reified RenderingT : Any> bindCompose(
8787
): ViewFactory<RenderingT> = ComposeViewFactory(RenderingT::class, showRendering)
8888

8989
@PublishedApi
90-
internal class ComposeViewFactory<RenderingT : Any>(
91-
override val type: KClass<RenderingT>,
90+
internal class ComposeViewFactory<in RenderingT : Any>(
91+
override val type: KClass<in RenderingT>,
9292
private val content: @Composable() (RenderingT, ViewEnvironment) -> Unit
9393
) : ViewFactory<RenderingT> {
9494

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.squareup.workflow.ui.compose
2+
3+
import androidx.animation.AnimatedFloat
4+
import androidx.animation.AnimationEndReason.TargetReached
5+
import androidx.compose.Composable
6+
import androidx.compose.getValue
7+
import androidx.compose.mutableStateOf
8+
import androidx.compose.onCommit
9+
import androidx.compose.setValue
10+
import androidx.ui.animation.animatedFloat
11+
import androidx.ui.core.DensityAmbient
12+
import androidx.ui.core.Modifier
13+
import androidx.ui.core.drawLayer
14+
import androidx.ui.unit.dp
15+
import kotlin.random.Random
16+
17+
/**
18+
* TODO write documentation
19+
*/
20+
class ExplodingViewFactoryTransformer(
21+
min: Int = -1,
22+
max: Int = 1
23+
) {
24+
25+
var min by mutableStateOf(min.toFloat())
26+
var max by mutableStateOf(max.toFloat())
27+
28+
val transformer: ViewFactoryTransformer = { _, _ -> transform() }
29+
30+
@Composable private fun transform(): Modifier {
31+
val offsetXDp = animatedFloat(0f)
32+
val offsetYDp = animatedFloat(0f)
33+
34+
onCommit(min, max) {
35+
if (min == 0f && max == 0f) {
36+
offsetXDp.animateTo(0f)
37+
offsetYDp.animateTo(0f)
38+
} else {
39+
offsetXDp.startPulseAnimation(min, max)
40+
offsetYDp.startPulseAnimation(min, max)
41+
}
42+
}
43+
44+
if (!offsetXDp.isRunning && !offsetYDp.isRunning) {
45+
println("OMG [invoke] not animating, returning early")
46+
return Modifier
47+
}
48+
49+
val offsetXPx = with(DensityAmbient.current) { offsetXDp.value.dp.toPx() }
50+
val offsetYPx = with(DensityAmbient.current) { offsetYDp.value.dp.toPx() }
51+
return Modifier.drawLayer(translationX = offsetXPx.value, translationY = offsetYPx.value)
52+
}
53+
54+
private fun AnimatedFloat.startPulseAnimation(
55+
min: Float,
56+
max: Float
57+
) {
58+
fun animate() {
59+
val target = Random.nextDouble(min.toDouble(), max.toDouble())
60+
.toFloat()
61+
animateTo(target) { reason, _ ->
62+
if (reason == TargetReached) {
63+
if (min != 0f || max != 0f) animate()
64+
}
65+
}
66+
}
67+
animate()
68+
}
69+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry")
2+
3+
package com.squareup.workflow.ui.compose
4+
5+
import androidx.compose.Composable
6+
import androidx.compose.remember
7+
import androidx.ui.core.Modifier
8+
import com.squareup.workflow.ui.ViewEnvironment
9+
import com.squareup.workflow.ui.ViewEnvironmentKey
10+
import com.squareup.workflow.ui.ViewFactory
11+
import com.squareup.workflow.ui.ViewRegistry
12+
import com.squareup.workflow.ui.compose.internal.showRendering
13+
import kotlin.reflect.KClass
14+
15+
/**
16+
* TODO write documentation
17+
*/
18+
typealias ViewFactoryTransformer = @Composable() (
19+
renderingDepth: Int,
20+
viewEnvironment: ViewEnvironment
21+
) -> Modifier
22+
23+
/**
24+
* TODO kdoc
25+
*/
26+
fun ViewRegistry.modifyViewFactories(transformer: ViewFactoryTransformer): ViewRegistry =
27+
TransformedViewRegistry(this, transformer)
28+
29+
private class TransformedViewRegistry(
30+
private val delegate: ViewRegistry,
31+
private val transformer: ViewFactoryTransformer
32+
) : ViewRegistry {
33+
override val keys: Set<KClass<*>> = delegate.keys
34+
35+
override fun <RenderingT : Any> getFactoryFor(
36+
renderingType: KClass<out RenderingT>
37+
): ViewFactory<RenderingT> {
38+
@Suppress("UNCHECKED_CAST")
39+
val realFactory = delegate.getFactoryFor(renderingType) as ViewFactory<Any>
40+
println("OMG [TVR] got real factory for $renderingType: $realFactory")
41+
RuntimeException().printStackTrace()
42+
43+
@Suppress("UNCHECKED_CAST")
44+
return ComposeViewFactory(renderingType as KClass<RenderingT>) { rendering, environment ->
45+
// No need to key depth on the environment, the depth will never change.
46+
val depth = remember { environment[FactoryDepthKey] }
47+
remember { println("OMG [TVR] creating CVF for depth $depth") }
48+
49+
val childEnvironment = remember(environment) {
50+
environment + (FactoryDepthKey to depth + 1)
51+
}
52+
val modifier = transformer(depth, environment)
53+
54+
// TODO this causes infinite recursion, because VF.showRendering calls
55+
// WorkflowViewStub.update, which will look up the factory again, get this one, try showing
56+
// again,
57+
realFactory.showRendering(rendering, childEnvironment, modifier)
58+
}
59+
}
60+
61+
/**
62+
* Values actually encode both depth and prevent infinite looping.
63+
* Even values mean the factory should do processing, odd values mean
64+
* direct pass-through.
65+
*/
66+
private object FactoryDepthKey : ViewEnvironmentKey<Int>(Int::class) {
67+
override val default: Int get() = 0
68+
}
69+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.squareup.workflow.ui.compose.internal
2+
3+
import android.content.Context
4+
import android.util.AttributeSet
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import com.squareup.workflow.ui.ViewEnvironment
8+
import com.squareup.workflow.ui.ViewFactory
9+
import com.squareup.workflow.ui.canShowRendering
10+
import com.squareup.workflow.ui.showRendering
11+
12+
/**
13+
* TODO write documentation
14+
*/
15+
internal class ForkedViewStub @JvmOverloads constructor(
16+
context: Context,
17+
attributeSet: AttributeSet? = null,
18+
defStyle: Int = 0,
19+
defStyleRes: Int = 0
20+
) : View(context, attributeSet, defStyle, defStyleRes) {
21+
init {
22+
setWillNotDraw(true)
23+
}
24+
25+
/**
26+
* On-demand access to the delegate established by the last call to [update],
27+
* or this [ForkedViewStub] instance if none has yet been set.
28+
*/
29+
var actual: View = this
30+
31+
/**
32+
* Replaces this view with one that can display [rendering]. If the receiver
33+
* has already been replaced, updates the replacement if it [canShowRendering].
34+
* If the current replacement can't handle [rendering], a new view is put in place.
35+
*
36+
* @return the view that showed [rendering]
37+
*
38+
* @throws IllegalArgumentException if no binding can be find for the type of [rendering]
39+
*
40+
* @throws IllegalStateException if the matching [ViewFactory] fails to call
41+
* [View.bindShowRendering] when constructing the view
42+
*/
43+
fun update(
44+
rendering: Any,
45+
viewEnvironment: ViewEnvironment,
46+
viewFactory: ViewFactory<Any>
47+
): View {
48+
actual.takeIf { it.canShowRendering(rendering) }
49+
?.let {
50+
it.showRendering(rendering, viewEnvironment)
51+
return it
52+
}
53+
54+
return when (val parent = actual.parent) {
55+
is ViewGroup -> viewFactory.buildView(rendering, viewEnvironment, parent.context, parent)
56+
.also { buildNewViewAndReplaceOldView(parent, it) }
57+
else -> viewFactory.buildView(rendering, viewEnvironment, actual.context)
58+
}.also { actual = it }
59+
}
60+
61+
private fun buildNewViewAndReplaceOldView(
62+
parent: ViewGroup,
63+
newView: View
64+
) {
65+
val index = parent.indexOfChild(actual)
66+
parent.removeView(actual)
67+
68+
actual.layoutParams
69+
?.let { parent.addView(newView, index, it) }
70+
?: run { parent.addView(newView, index) }
71+
}
72+
}

core-compose/src/main/java/com/squareup/workflow/ui/compose/internal/ViewFactories.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import com.squareup.workflow.ui.compose.internal.ComposableViewStubWrapper.Updat
6262
// IntelliJ currently complains very loudly about this function call, but it actually
6363
// compiles. The IDE tooling isn't currently able to recognize that the Compose compiler
6464
// accepts this code.
65-
ComposableViewStubWrapper(update = Update(rendering, newEnvironment))
65+
ComposableViewStubWrapper(update = Update(rendering, newEnvironment, this))
6666
}
6767
}
6868
}
@@ -79,18 +79,19 @@ private class ComposableViewStubWrapper(context: Context) : FrameLayout(context)
7979

8080
data class Update(
8181
val rendering: Any,
82-
val viewEnvironment: ViewEnvironment
82+
val viewEnvironment: ViewEnvironment,
83+
val viewFactory: ViewFactory<Any>
8384
)
8485

85-
private val viewStub = WorkflowViewStub(context)
86+
private val viewStub = ForkedViewStub(context)
8687

8788
init {
8889
addView(viewStub)
8990
}
9091

9192
// Compose turns this into a parameter when you invoke this class as a Composable.
9293
fun setUpdate(update: Update) {
93-
viewStub.update(update.rendering, update.viewEnvironment)
94+
viewStub.update(update.rendering, update.viewEnvironment, update.viewFactory)
9495
}
9596

9697
override fun getLayoutParams(): LayoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)

samples/nested-renderings/src/main/java/com/squareup/sample/nestedrenderings/NestedRenderingsActivity.kt

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,51 @@ package com.squareup.sample.nestedrenderings
1818
import android.os.Bundle
1919
import androidx.appcompat.app.AppCompatActivity
2020
import androidx.compose.Providers
21+
import androidx.compose.getValue
22+
import androidx.compose.setValue
23+
import androidx.compose.state
24+
import androidx.ui.core.setContent
2125
import androidx.ui.graphics.Color
26+
import androidx.ui.layout.Column
27+
import androidx.ui.material.Slider
2228
import com.squareup.workflow.diagnostic.SimpleLoggingDiagnosticListener
2329
import com.squareup.workflow.ui.ViewEnvironment
2430
import com.squareup.workflow.ui.ViewRegistry
25-
import com.squareup.workflow.ui.WorkflowRunner
31+
import com.squareup.workflow.ui.compose.ExplodingViewFactoryTransformer
32+
import com.squareup.workflow.ui.compose.WorkflowContainer
33+
import com.squareup.workflow.ui.compose.modifyViewFactories
34+
import com.squareup.workflow.ui.compose.showRendering
2635
import com.squareup.workflow.ui.compose.withComposeViewFactoryRoot
27-
import com.squareup.workflow.ui.setContentWorkflow
2836

2937
private val viewRegistry = ViewRegistry(
3038
RecursiveViewFactory,
3139
LegacyRunner
3240
)
41+
private val exploder = ExplodingViewFactoryTransformer()
3342

34-
private val viewEnvironment = ViewEnvironment(viewRegistry).withComposeViewFactoryRoot { content ->
43+
private val viewEnvironment = ViewEnvironment(
44+
viewRegistry.modifyViewFactories(exploder.transformer)
45+
).withComposeViewFactoryRoot { content ->
3546
Providers(BackgroundColorAmbient provides Color.Green, children = content)
3647
}
3748

3849
class NestedRenderingsActivity : AppCompatActivity() {
3950
override fun onCreate(savedInstanceState: Bundle?) {
4051
super.onCreate(savedInstanceState)
41-
setContentWorkflow(viewEnvironment) {
42-
WorkflowRunner.Config(
43-
RecursiveWorkflow,
44-
diagnosticListener = SimpleLoggingDiagnosticListener()
45-
)
52+
setContent {
53+
var vibrateRange by state { 0f }
54+
Column {
55+
Slider(value = vibrateRange, onValueChange = {
56+
vibrateRange = it
57+
exploder.min = -it
58+
exploder.max = it
59+
}, valueRange = 0f..10f)
60+
61+
WorkflowContainer(
62+
RecursiveWorkflow,
63+
diagnosticListener = SimpleLoggingDiagnosticListener()
64+
) { viewEnvironment.showRendering(it) }
65+
}
4666
}
4767
}
4868
}

0 commit comments

Comments
 (0)