Giter Club home page Giter Club logo

contour's Introduction

Contour [DEPRECATED]

Contour is deprecated. It has been amazing. It is stable and probably still running in the wild. But its development has ceased. We encourage developers to favor Jetpack Compose UI.

Contour is a typesafe, Kotlin-first API that aims to be the thinnest possible wrapper around Android’s layout APIs. It allows you to build compound views in pure Kotlin without using opaque layout rules - but instead by hooking into the layout phase yourself. The best comparison for Contour would be to ConstraintLayout - but instead of defining constraints in XML you actually provide them as executable lambdas.

Deprecating XML

XML had a good ride but times have changed and we should too. XML is a decent format for declaring static content - but that’s not what UIs are anymore. UIs increasingly have a ton of dynamic behavior - and trying to jerry-rig XML layouts into adopting this dynamic behavior has taken things from bad to worse. What sort of dynamic behaviors do we expect from our UIs?

  • Change configuration based on screen size
  • Lazy loading elements of the UI.
  • A / B testing components at runtime
  • Dynamic theming
  • Add & remove components based on app state

Let’s face it - XML is a static markup language and we’re trying to make it a full-fledged programming language. What’s the alternative? A full-fledged programming language! Kotlin! What sort of things do we get by abandoning the XML model?

  • No findViewById - view declaration exists in scope.
  • No R.id.my_view ambiguity - Kotlin has namespacing & encapsulation!
  • Static typing!
  • Great IDE Support (No more laggy xml editor)
  • ViewHolders aren’t needed
  • Easier DI

Usage

Similar to ConstraintLayout, Contour works by creating relationships between views' position and size. The difference is, instead of providing opaque XML attributes, you provide functions which are directly called during the layout phase. Contour calls these functions "axis solvers" and they're provided using layoutBy(). Here's an example:

class BioView(context: Context) : ContourLayout(context) {
  private val avatar = ImageView(context).apply {
    scaleType = CENTER_CROP
    Picasso.get().load("https://i.imgur.com/ajdangY.jpg").into(this)
  }

  private val bio = TextView(context).apply {
    textSize = 16f
    text = "..."
  }

  init {
    avatar.layoutBy(
      x = leftTo { parent.left() }.widthOf { 60.xdip },
      y = topTo { parent.top() }.heightOf { 60.ydip }
    )
    bio.layoutBy(
      x = leftTo { avatar.right() + 16.xdip }.rightTo { parent.right() },
      y = topTo { parent.top() }
    )
    contourHeightOf { bio.bottom() }
  }
}

Runtime Layout Logic

Because you're writing plain Kotlin, you can reference other Views and use maths in your lambdas for describing your layout. These lambdas can also be used for making runtime decisions that will be evaluated on every layout pass.

bio.layoutBy(
  x = ...
  y = topTo {
    if (isSelected) parent.top() + 16.ydip
    else avatar.centerY() - bio.height() / 2
  }.heightOf {
    if (isSelected) bio.preferredHeight()
    else 48.ydip
  }
)

When paired with an animator, your runtime layout decisions can even act as keyframes for smoothly updating your layout.

setOnClickListener {
  TransitionManager.beginDelayedTransition(this)
  isSelected = !isSelected
  requestLayout()
}

What does the end result of this look like?

contourlayout animation

Context-Aware API

Contour tries to make it easy to do the right thing. As part of this effort, all of the layout functions return interfaces as views of the correct available actions.

For example, when defining a constraint of leftTo, the only exposed methods to chain in this layout are rightTo or widthOf. Another leftTo, or centerHorizontallyTo don't really make sense in this context and are hidden. In short:

layoutBy(
  x = leftTo { name.left() }.leftTo { name.right() },
  y = topTo { name.bottom() }
)

will not compile.

Axis Type Safety

Contour makes heavy use of inline classes to provide axis type safety in layouts. What this means is,

toLeftOf { view.top() }

will not compile either. toLeftOf {} requires a XInt, and top() returns a YInt. In cases where this needs to be forced, casting functions are made available to toY() & toX().

Inline classes are a lightweight compile-time addition that allow this feature with minimal to no performance costs. https://kotlinlang.org/docs/reference/inline-classes.html

Circular Reference Debugging

Circular references are pretty easy to unintentionally introduce in any layout. To accidentally declare

  • name.right aligns to note.left
  • and note.left aligns to name.right

Contour fails fast and loud when these errors are detected, and provides as much context as possible when doing so. The screenshot below is an example of the trace provided when a circular reference is detected.

Comparison with Compose

There is a lot of buzz and interest around writing views in code right now with the development of Jetpack Compose. Compose is a programmatic UI toolkit that uses reactive programming to drive the views. In contrast Contour doesn’t care about the update mechanisms - whether they be FRP or plain old imperative. Contour is only concerned with the nuts and bolts of view layouts - and making them as flexible and easy as possible.

The only similarity between Contour and Compose is that they both realize writing layouts in Kotlin is maximum cool.

Releases

implementation "app.cash.contour:contour:1.1.0"

Snapshots of the development version are available in Sonatype's snapshots repository.

repositories {
  mavenCentral()
  maven {
    url 'https://oss.sonatype.org/content/repositories/snapshots/'
  }
}

License

Copyright 2019 Square, Inc.

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.

contour's People

Contributors

aerb avatar aleckazakova avatar carranca avatar dimsuz avatar dongheeleeme avatar egorand avatar jakewharton avatar jamolkhon avatar jingibus avatar jrodbx avatar kunlin666 avatar louiscad avatar msfjarvis avatar oldergod avatar orgmir avatar saket avatar shenghaiyang avatar vitusortner avatar zacsweers avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

contour's Issues

Easier MATCH_PARENT AxisSolver

Is there an easier way to achieve a MATCH_PARENT parameter for widths and heights?

So far I've done it this way:

applyLayout(
    x = leftTo { parent.left() }
        .rightTo { parent.right() },
        y = topTo { parent.top() }
    )

But it is not straightforward and it looks like a workaround.

ContourLayout and existing Layouts

Is it possibile to add a ContourLayout to an existing layout? When i try to add it to a FrameLayout i obtain ths exception:

java.lang.ClassCastException: com.squareup.contour.ContourLayout$LayoutSpec cannot be cast to android.view.ViewGroup$MarginLayoutParams

Allow opting out of padding-conscious layout behaviour

In #28 we introduced automatically taking view padding into account when laying out subviews.

For existing code, this might require some migration work, and we don't want adoption of the latest and greatest release of Contour to be hindered by this.

Therefore, let's add a new field to ContourLayout that goes like this:

@Deprecated("Migrate view to properly take padding into account or explicitly null it out")
val respectPadding: Boolean = true

When this field is set to false, the view's padding fields should not be taken into account anywhere in the Contour stack.

View state saving inside ContourLayout

We recently started using contour and are really liking the no-XML approach. However, I'm a little confused as to the best way of saving state.

Say I'm using ContourLayout for my fragment like so:

class MyFragmentView(context: Context) : ContourLayout(context) {
    val editText1
    val editText2
    val editText3

    override fun onInitializeLayout() {
        // applyLayout calls
    }
}

and then in my Fragment,

class MyFragment : Fragment() {
    private lateinit var contourView: MyFragmentView

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = MyFragmentView(requireContext()).apply {
        contourView = this
    }

    // other fragment functions
}

Now if the fragment view hierarchy is re-created (say via the Developer Options setting "Don't keep activities"), naturally, user input in the editTexts is not restored.

To get around this, we can do:

class MyFragment : Fragment() {
    // other fragment functions

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("input1", contourView.editText1.getText())
        outState.putString("input2", contourView.editText2.getText())
        outState.putString("input3", contourView.editText3.getText())
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        savedInstanceState?.let { 
            contourView.editText1.setText(it.getString("input1"))
            contourView.editText2.setText(it.getString("input1"))
            contourView.editText3.setText(it.getString("input1"))
        }
    }
}

But I have a gut feeling this may not be the best way. Is there some way view state restoration can happen internally inside ContourLayout given the way we're using it in MyFragment above?

Allow constraints to use the other axis

It would be pretty cool to use information about the other Axis to build constraints.
E.g. Imagine building a square like:

    private val square = View(context).applyLayout(
        x = leftTo { parent.left() }.rightTo { parent.right() },
        y = topTo { parent.bottom() }.heightOf { width().toY() }
    )
 

This would currently fail, because of "circular reference".

Best practices for layout preview?

Given that XML isn't involved, the layout preview in studio doesn't apply here - what's the recommended way of previewing contour layouts (if any) besides building/deploying?
Just curious if the cash app team has the same problem, has found another solution, or if its not really an issue re-deploying after making changes.

Thanks!

Contour Debug Mode

The idea here would be to print to logcat whenever the ContourLayout or one of its direct subviews has its position or size recalculated, i.e. whenever one of the lambdas defined in the context of that layout gets run.

It would be useful for debugging view layouts (especially for more complex layouts or layouts whose size changes frequently.

Remove `onInitializeLayout()`

Discussion in #11 has revealed that it's not really a good idea to keep it around. I'm deprecating it as part of #75 but it should be removed altogether before we go public with 1.0.0

Readme Example Seems Wrong

In the code, the example shows the name being constrained to the top:

y = topTo { parent.top() + 15.dip }

With the avatar being constrained below it:

y = topTo { name.bottom() }

But the gif shows it constrained to the bottom with the avatar above it.

Allow overriding height iff it's greater than the parent height

Scenario:
ContourLayout inside of a ScrollView.

Wanted API (name TBD):

init {
  contourMaxHeightOf { lastView.bottom() }
}

Behavior:
Sets height to max(availableSpace, lambda)

I tried this using contourHeightOf and couldn't get it to work:

contourHeightOf { maxOf((parent as ViewGroup).height.toYInt(), lastView.bottom()) }

Allow aligning of a view to a group of views

This is something accomplished using Barrier in ConstraintLayout, but I think we can make it more straightforward.

Given views A, B, C:

Scenarios

  1. Aligning a view A to at any given time be to the left of both B and C
  2. Aligning a view A to at any given time be to the right of both B and C
  3. Aligning a view A to at any given time be horizontally centered to the middle point between B and C
  4. Aligning a view A to at any given time be above both B and C
  5. Aligning a view A to at any given time be below both B and C
  6. Aligning a view A to at any given time be vertically centered to the middle point between B and C

When the layout of B or C is updated, the positioning of A must be updated.

Skip layout of children with visibility set to GONE

When a child View's visibility is set to GONE, traditional layouts skip laying the child and measure it as 0 width & height. One can argue that this is kind of an implicit behavior, but given that other layouts have always carried this behaved, should we consider matching this expected behavior?

Problem with kotlin 1.4.30

java.lang.NoSuchMethodError: No virtual method getYdip-dBGyhoQ(I)I in class

Tried with contour 1.1.0-SNAPSHOT as well, same error.

When I downgrade kotlin to 1.4.20 everything works.

Default layout width should be consistent

I needed a textView and a button, one below the other, both right aligned in the parent (while still respecting the left constraint of the parent's edge).

For the textView, I did the following and it worked:

textView.applyLayout(
    rightTo { parent.right() - 32.dip }.leftTo { parent.left() + 32.dip },
    // some YAxisSolver
)

The textView was wrap_content and right aligned, and if the text grew to multiple lines, the left constraint was still respected. However, doing the same for the button resulted in an always-match_parent button, even if the contents inside were much narrower. To achieve what I wanted, I had to use SizeMode.AtMost like so:

button.applyLayout(
    rightTo { parent.right() - 32.dip }.leftTo(SizeMode.AtMost) { parent.left() + 32.dip },
    topTo { textView.bottom() + 16.dip }
)

Why is this the case? Shouldn't the default width strategy be the same for all view types?

Does contour have a performance benchmark

I want to know the performance compare between contour vs constraintLayout. I have seen in some places that if a simple layout uses constraintLayout, it will not be faster than using frameLayout or linearLayout, so contour?

How to make the sub view use native wrap_content?

The layout api must specify the solver for the x and y axes. If I want the x axis to have similar behaviors like wrap_content, what should I do?

I only saw matchParentX/Y, why not provide wrapContentX/Y?

In native, if you don't specify width or height, LayoutParams.WRAP_CONTENT will be used by default. Can contour do the same thing?

Anchor contents to bottom

I'm using ContourLayout to build a view that will be placed inside a ScrollView (with fillViewport="true"). When scrolling is not required, I'd like for the contents to be anchored to the bottom of the ScrollView.

Here is how I'm going about it (for simplicity, let's just say there are 2 children views):

class MyContourView(
    context: Context,
    private val view1: View,
    private val view2: View,
    private val interItemMarginPx: Int,
    private val lrMarginPx: Int
) : ContourLayout(context) {
    init {
        contourHeightOf { availableHeight ->
            maxOf(availableHeight, view2.bottom() + interItemMarginPx
        }
    }

    override fun onInitializeLayout() {
        view2.applyLayout(
            leftTo { parent.left() + lrMarginPx }.rightTo { parent.right() - lrMarginPx },
            bottomTo { parent.bottom() - interItemMarginPx }
        )

        view1.applyLayout(
            leftTo { parent.left() + lrMarginPx }.rightTo { parent.right() - lrMarginPx },
            bottomTo { view2.top() - interItemMarginPx }
        )
    }
}

But I get a CircularReferenceDetected error (at the line where I call view2.bottom() in contourHeightOf {}). I can see why that's the case: view2 references parent.bottom() and the parent references view2.bottom() in determining its height.

Everything works if I remove the contourHeightOf {} block -- in which case it's always going to be availableHeight. But this breaks the scrolling if the height of the contents is greater than availableHeight.

How do I go about this use case?

wrap_content

Hi! Using 2 nested contour layout, is it possible to have something like wrap_content?

Add compareTo operator for XInt/YInt

There are certain scenarios (like the one described in #59) in which it is handy to do something like this:

if (view1.bottom() < view2.bottom()) view1.bottom() else otherView.top()

Currently XInt/YInt lack operator fun compareTo which prevents the above from compiling.
The workaround could be to use minOf(view1.bottom() - view2.bottom(), 0.ydip) == 0.ydip but that's a bit ugly.

Can those be added (I can forge a PR) or do you have something specific in mind why this is not a good idea?

Is Contour how Cash app is built?

Just wanted to understand if that's the case. And whether you are planning to improve and promote this library more for Android UI development.

Understanding Visibility Changes, Layout, and maxOf/minOf

Overview:

I'm trying to achieve an effect similar to #116, but with the maxOf function. Unfortunately for me, I'm a little bit confused by Contour's behavior when I change the visibility of one of the optional views.

Steps:

We can easily reproduce the behavior that's confusing me using the sample app.

  • Add private val card3 = ExpandableBioCard1(context) to SampleView:
  • Add this to SampleView's init block
card3.layoutBy(
 x = matchParentX(marginLeft = 24.dip, marginRight = 24.dip),
 y = maxOf(topTo { card2.bottom() + 24.ydip }, topTo { card1.bottom() + 24.ydip })
)
  • Add isVisible = false to ExpandableBioCard2's init block (in my real app this can happen in the render method, but it has the same effect here)
  • Notice that there is additional space in between card3 and card1

(Possibly Misguided) Expectations:

I expected card2's visibility change to GONE to result in card2.bottom() evaluating to 0, causing maxOf to pick the second argument.
I expected this for two reasons:

  • The docs for View.GONE state that the "view is invisible, and it doesn't take any space for layout purposes"
  • In a LinearLayout that contains two views, if you set the visibility of the first view to GONE the second view automatically takes its place

Of course, this is not what's happening. maxOf is always picking topTo { card2.bottom() + 24.ydip } because while card2.height()=0, card2.bottom() is still > card1.bottom().

Current (Possibly Permanent) Workaround:

I can get the desired behavior by doing this:

card3.layoutBy(
  x = matchParentX(marginLeft = 24.dip, marginRight = 24.dip),
  y = topTo {
    when {
       card2.isVisible -> card2.bottom() + 24.ydip
       else -> card1.bottom() + 24.ydip
    }
  }
)

Questions:

  • Is this working as intended? Why does card2.bottom() not evaluate to 0?
  • Is checking the visibility in topTo the recommended approach for achieving this effect?

Thank you all for this amazing library! I won't ever write another layout in XML if I can help it 😄 .

Int.xdip/Int.ydip & Float.xdip/Float.ydip cause crash on Kotlin 1.4.30

This can be easily reproduced by changing the dependency on the sample app from the module to the binary:
implementation project(':contour') -> implementation 'app.cash.contour:contour:1.0.0'

Here's the stack trace:

java.lang.NoSuchMethodError: No virtual method getYdip-dBGyhoQ(I)I in class 
Lcom/squareup/contour/sample/SampleView; or its super classes (declaration of 
'com.squareup.contour.sample.SampleView' appears in /data/app/~~0Ba-
93BI78uARYazH4agJA==/com.squareup.contour.sample-VKrVvhwusycb2ElCbF70RQ==/base.apk!classes2.dex)
        at com.squareup.contour.sample.SampleView$3.invoke(SampleView.kt:56)

Int.dip doesn't seem to be an issue.

At least on my project, reverting to 1.4.21 fixes the issue immediately.

java.lang.NoSuchMethodError: No virtual method bottom-h0YXg9w(Landroid/view/View;)I in class

kotlin 1.4.0

works fine on 1.3.72

stack trace:

java.lang.NoSuchMethodError: No virtual method bottom-h0YXg9w(Landroid/view/View;)I in class Lcom/priceline/android/contour/CollectionsView; or its super classes (declaration of 'com.priceline.android.contour.CollectionsView' appears in /data/app/com.priceline.android.contour-jK1itqBV6DI3fBgme768dQ==/base.apk!classes2.dex)
at com.priceline.android.contour.CollectionsView$4.invoke(CollectionsView.kt:16)
at com.priceline.android.contour.CollectionsView$4.invoke(CollectionsView.kt:7)
at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntToYIntLambda$1.invoke(XYIntUtils.kt:60)
at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntToYIntLambda$1.invoke(Unknown Source:6)
at com.squareup.contour.constraints.SizeConfig.resolve(SizeConfig.kt:27)
at com.squareup.contour.ContourLayout.onMeasure(ContourLayout.kt:194)
at android.view.View.measure(View.java:25086)
at androidx.constraintlayout.widget.ConstraintLayout$Measurer.measure(ConstraintLayout.java:763)
at androidx.constraintlayout.solver.widgets.analyzer.BasicMeasure.measure(BasicMeasure.java:426)
at androidx.constraintlayout.solver.widgets.analyzer.BasicMeasure.measureChildren(BasicMeasure.java:105)
at androidx.constraintlayout.solver.widgets.analyzer.BasicMeasure.solverMeasure(BasicMeasure.java:247)
at androidx.constraintlayout.solver.widgets.ConstraintWidgetContainer.measure(ConstraintWidgetContainer.java:117)
at androidx.constraintlayout.widget.ConstraintLayout.resolveSystem(ConstraintLayout.java:1532)
at androidx.constraintlayout.widget.ConstraintLayout.onMeasure(ConstraintLayout.java:1607)
at android.view.View.measure(View.java:25086)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6872)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
at androidx.appcompat.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:146)
at android.view.View.measure(View.java:25086)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6872)
at androidx.appcompat.widget.ActionBarOverlayLayout.onMeasure(ActionBarOverlayLayout.java:490)
at android.view.View.measure(View.java:25086)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6872)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
at android.view.View.measure(View.java:25086)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6872)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:842)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
at android.view.View.measure(View.java:25086)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6872)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
at com.android.internal.policy.DecorView.onMeasure(DecorView.java:742)
at android.view.View.measure(View.java:25086)
at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:3083)
at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1857)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2146)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1745)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7768)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:967)
at android.view.Choreographer.doCallbacks(Choreographer.java:791)
at android.view.Choreographer.doFrame(Choreographer.java:726)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:952)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

Crash XYIntUtils kotlin 1.4.30

.FooterView$4.invoke(FooterView.kt:47)
        at ....FooterView$4.invoke(FooterView.kt:13)
        at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntLambda$1.invoke(XYIntUtils.kt:51)
        at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntLambda$1.invoke(Unknown Source:2)
        at com.squareup.contour.constraints.Constraint.resolve(Constraint.kt:48)
        at com.squareup.contour.solvers.SimpleAxisSolver.measureSpec(SimpleAxisSolver.kt:173)
        at com.squareup.contour.ContourLayout$LayoutSpec.measureSelf$contour_release(ContourLayout.kt:758)
        at com.squareup.contour.solvers.SimpleAxisSolver.max(SimpleAxisSolver.kt:110)
        at com.squareup.contour.ContourLayout$LayoutSpec.bottom-h0YXg9w$contour_release(ContourLayout.kt:729)
        at com.squareup.contour.ContourLayout.bottom-h0YXg9w(ContourLayout.kt:469)
        at ....FooterView$6.invoke(FooterView.kt:52)
        at ....FooterView$6.invoke(FooterView.kt:13)
        at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntLambda$1.invoke(XYIntUtils.kt:51)
        at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntLambda$1.invoke(Unknown Source:2)
        at com.squareup.contour.constraints.Constraint.resolve(Constraint.kt:48)
        at com.squareup.contour.solvers.SimpleAxisSolver.resolveAxis(SimpleAxisSolver.kt:131)
        at com.squareup.contour.solvers.SimpleAxisSolver.max(SimpleAxisSolver.kt:111)
        at com.squareup.contour.ContourLayout$LayoutSpec.bottom-h0YXg9w$contour_release(ContourLayout.kt:729)
        at com.squareup.contour.ContourLayout.bottom-h0YXg9w(ContourLayout.kt:469)
        at ....FooterView$11.invoke(FooterView.kt:62)
        at ....FooterView$11.invoke(FooterView.kt:13)
        at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntLambda$1.invoke(XYIntUtils.kt:51)
        at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntLambda$1.invoke(Unknown Source:2)
        at com.squareup.contour.constraints.Constraint.resolve(Constraint.kt:48)
        at com.squareup.contour.solvers.SimpleAxisSolver.resolveAxis(SimpleAxisSolver.kt:131)
        at com.squareup.contour.solvers.SimpleAxisSolver.max(SimpleAxisSolver.kt:111)
        at com.squareup.contour.ContourLayout$LayoutSpec.bottom-h0YXg9w$contour_release(ContourLayout.kt:729)
        at com.squareup.contour.ContourLayout.bottom-h0YXg9w(ContourLayout.kt:469)
        at ....FooterView$16.invoke(FooterView.kt:75)
        at ....FooterView$16.invoke(FooterView.kt:13)
        at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntToYIntLambda$1.invoke(XYIntUtils.kt:61)
        at com.squareup.contour.utils.XYIntUtilsKt$unwrapYIntToYIntLambda$1.invoke(Unknown Source:6)
        at com.squareup.contour.constraints.SizeConfig.resolve(SizeConfig.kt:29)
        at com.squareup.contour.ContourLayout.onMeasure(ContourLayout.kt:213)
        at android.view.View.measure(View.java:25466)

Usage in lists

Some of the points that are great with contour which are stated in the readme are:

  • ViewHolders aren’t needed

  • Add & remove components based on app state

  • Lazy loading elements of the UI.

These points together sound like this layout could replace recycler views.

In the application I'm currently working on about 95% of the screens are scrollable lists, so basically every screen is made of a single RecyclerView.

How is this library designed to be used? Would you use one contourlayout inside a each ViewHolder?

Nested ContourLayout causing StackOverflow

The following code causes a stack overflow during layout:

    class ParentView(context: Context): ContourLayout(context) {
        init {
            setBackgroundColor(Color.RED)
        }

        val childView = ChildView(context).apply {
            applyLayout(
                x = leftTo { parent.left() }.widthOf { 100.xdip },
                y = topTo { parent.top() }.heightOf { 100.ydip }
            )
        }
    }

    class ChildView(context: Context) : ContourLayout(context) {
        init {
            setBackgroundColor(Color.BLUE)
        }
    }

However, moving ChildView's applyLayout into it's onInitializeLayout solves the issue:

    class ParentView(context: Context): ContourLayout(context) {
        init {
            setBackgroundColor(Color.RED)
        }

        val childView = ChildView(context).also { addView(it) }
    }

    class ChildView(context: Context) : ContourLayout(context) {
        init {
            setBackgroundColor(Color.BLUE)
        }

        override fun onInitializeLayout() {
            super.onInitializeLayout()
            applyLayout(
                x = leftTo { parent.left() }.widthOf { 100.xdip },
                y = topTo { parent.top() }.heightOf { 100.ydip }
            )
        }
    }

I don't know if this is a bug or not, but my hunch is it's the intended behavior (if not, I'd be happy to look into it).

I realize nesting layouts is discouraged, but there are going to be situations where it is useful and people are going to do it anyway.

The pattern Contour uses for configuring child views (MyView(context).apply { applyLayout() }) is great, and it seems clunky to have to violate it iff the child happens to be another ContourLayout (I can imagine during refactoring a View that didn't start as Contour might become one later).

Could we make it so layout constraints declared at construction time get deferred until onInitializeLayout? Either by having applyLayout behave differently for ContourLayouts or with an onInitializeLayout DSL:

    class ParentView(context: Context): ContourLayout(context) {
        init {
            setBackgroundColor(Color.RED)
        }

        val childView = ChildView(context).apply {
            onInitializeLayout {
                applyLayout(
                    x = leftTo { parent.left() }.widthOf { 100.xdip },
                    y = topTo { parent.top() }.heightOf { 100.ydip }
                )
            }
        }
    }

    class ChildView(context: Context) : ContourLayout(context) {
        init {
            setBackgroundColor(Color.BLUE)
        }
    }

Usage

Hi, may you update readme about setup contour dependency from scratch please

Question: How to understand ContourLayout.invoke()?

In SampleActivity, there's views[i]() which calls invoke().
Using breakpoints, it seems invoke() reinstantiate everything in SampleView1 and SampleView2
Is that correct?
Or is invoke() kinda redraw the view with a new state or sth like that?

Couldn't find invoke() function defined inside ContourLayout that's why I couldn't help myself.
Are there use cases for parameters to be passed to the ContourLayout.invoke() function?

Lint error when adding a view manually to a contour layout.

This would otherwise crash when casting the LayoutSpec

  private inline fun View.spec(): LayoutSpec {
    if (parent !== this@ContourLayout) {
      throw IllegalArgumentException("Referencing view outside of ViewGroup.")
    }
    return layoutParams as LayoutSpec
  }

How to update an existing view?

It could be answered by #31
But let me single out this question here:
Consider:

private val name: TextView =
    TextView(context).apply {
      text = "Ben Sisko"
      setTextColor(White)
      setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18f)
      applyLayout(
        x = leftTo { parent.left() + 15.dip },
        y = topTo { parent.top() + 15.dip }
      )
    }

How to update that name TextView to "John Doe"?

How to handle multiple gone views?

I have a header and chip group that can be visible, or gone. What is the best way to handle this and not have views overlap? It seems that if the view that visible relies on the view that gone it doesn't always evaluate correct.

Here is the sample in the init of the conditional fields. if for example there is no demographic then i hide the text and chips. This seems to work in some scenarios. One that does does not work is if contentType, format, theme are gone. Then the genre text overlaps in the demographic chip.

        demographicText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { descriptionExpanded.bottom() }
        )
        demographicChips.layoutBy(
            x = matchParentX(),
            y = topTo { demographicText.bottom() - 8.ydip }
        )

        contentTypeText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { demographicChips.bottom() }
        )
        contentTypeChips.layoutBy(
            x = matchParentX(),
            y = topTo { contentTypeText.bottom() - 8.ydip }
        )
        formatText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { contentTypeChips.bottom() }
        )
        formatChips.layoutBy(
            x = matchParentX(),
            y = topTo { formatText.bottom() - 8.ydip }
        )
        themeText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { formatChips.bottom() }
        )
        themeChips.layoutBy(
            x = matchParentX(),
            y = topTo { themeText.bottom() - 8.ydip }
        )
        genreText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { themeChips.bottom() }
        )
        genreChips.layoutBy(
            x = matchParentX(),
            y = topTo { genreText.bottom() - 8.ydip }
        )
        altTitlesText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { genreChips.bottom() }
        )
        altTitles.layoutBy(
            x = matchParentX(),
            y = topTo { altTitlesText.bottom() - 12.ydip }
        )

Add an example of compareable solvers in a README?

I've only now watched Adam's presentation about Contour and while I was using it for some time now, this slide blew my mind

I've been occasionally struggling with the fact that sometimes I want one solver and sometimes (depending on view config) another. As a result I had to invent some clever tricks, while I didn't know about this feature.

Can it be somehow made more obvious? Perhaps a mention in README or in some other documentation source?

Add support for more than 2 arguments to minOf/maxOf (both for XInt/YInt, XAxisSolver/YAxisSolver variants)

It is not the first time I have this usecase, where I want more than two arguments for minOf.

minOf currently supports XInt/YInt and XAxisSolver/YAxisSolver (but only 2 args for both kinds)...

I have just ran into another case with the latter. The layout is like this:

TITLE
PRIMARY_ACTION (optional, maybe GONE)
SECONDARY_ACTION (optional, maybe GONE)
---screen-bottom---

I want to do something like this:

titleView.layoutBy(
  x = matchParentX(),
  y = minOf(
     bottomTo { primaryAction.top() - 16.ydip },
     bottomTo { secondaryAction.top() - 16.ydip },
     bottomTo { parent.bottom() - 16.ydip },
  )
)

but minOf has only 2 args... Similar situation is for xint/yint, I often find myself wishing for more args.

Is this possible or maybe I also miss some easy workaround to achive what I want? I know I can put if (someView.isVisible) rule1 else rule2 in my lambdas, but that looks not very elegant and a bit messy...

P.S. Can't help but think that if we'd be in the functional programming land, XInt/Yint, and axis solvers could be an instance of Monoid + Ord and all the stdlib functions (including min) would just work on them. But oh well :)

Is 0.1.8 version released for public use?

CHANGELOG.md Suggests that 0.1.8 version was released on 2020-06-11.
But I could not find it in maven.
Also README.md points to 0.1.7 version. And that makes me wonder 0.1.8 version is released for public use?

Support foreground on API below 23

Since ContourLayout extends ViewGroup, it doesn't support foreground on API below 23, a common workaround is to wrap the layout in a FrameLayout, but this seems redundant

Text is cut during transition

After running the Sample app without any modifications, and clicking the first card; I've noticed that the text is cut by some margin (I think) during the transition.

See the following video: https://imgur.com/amsY2I8

The avatar is acting normally. It is "cut" by the card only at the very bottom.

I'm not sure what's causing this bug as I'm not very familiar with Contour yet, but I believe it could be because of the centerY() part, on this line:

else -> avatar.centerY() - bio.preferredHeight() / 2

Right to left layouts support

Hi folks! 🙂

I just wanted to ask if it's in the plans to add support for right to left layouts in the library. I guess that library users are mostly interested in supporting LTR locales, but I was wondering what are your thoughts/plans on this.

Recipes section in README

We've discussed creating a Recipes section in the README for some patterns of use of Contour APIs that aren't necessarily the most straightforward.

One such example is, in the words of @aerb ,

if you find yourself wanting to use measuredWidth / measuredHeight there is an extension you can use called preferredWidth / preferredHeight on Contour which will trigger a measure() without requiring a full layout. That should help you avoid scenarios where you are asking for measuredWidth before measure has occurred

Don't suppress warnings for experimental Kotlin features

Since Contour is using experimental features (inline classes), it should be either propagating the warnings through annotations or Gradle flags (see Experimental API markers), or introducing its own experimental annotation (see an example of this in KotlinPoet). An argument in favor of a library-specific annotation is that developers might be fine with using experimental features in their own code, but reluctant to use 3rd party libraries that use them, hence the library should require explicit opt-in.

I have a header and chip group that can be visible, or gone. What is the best way to handle this and not have views overlap? It seems that if the view that visible relies on the view that gone it doesn't always evaluate correct.

I have a header and chip group that can be visible, or gone. What is the best way to handle this and not have views overlap? It seems that if the view that visible relies on the view that gone it doesn't always evaluate correct.

Here is the sample in the init of the conditional fields. if for example there is no demographic then i hide the text and chips. This seems to work in some scenarios. One that does does not work is if contentType, format, theme are gone. Then the genre text overlaps in the demographic chip.

        demographicText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { descriptionExpanded.bottom() }
        )
        demographicChips.layoutBy(
            x = matchParentX(),
            y = topTo { demographicText.bottom() - 8.ydip }
        )

        contentTypeText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { demographicChips.bottom() }
        )
        contentTypeChips.layoutBy(
            x = matchParentX(),
            y = topTo { contentTypeText.bottom() - 8.ydip }
        )
        formatText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { contentTypeChips.bottom() }
        )
        formatChips.layoutBy(
            x = matchParentX(),
            y = topTo { formatText.bottom() - 8.ydip }
        )
        themeText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { formatChips.bottom() }
        )
        themeChips.layoutBy(
            x = matchParentX(),
            y = topTo { themeText.bottom() - 8.ydip }
        )
        genreText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { themeChips.bottom() }
        )
        genreChips.layoutBy(
            x = matchParentX(),
            y = topTo { genreText.bottom() - 8.ydip }
        )
        altTitlesText.layoutBy(
            x = leftTo { parent.left() },
            y = topTo { genreChips.bottom() }
        )
        altTitles.layoutBy(
            x = matchParentX(),
            y = topTo { altTitlesText.bottom() - 12.ydip }
        )

Originally posted by @CarlosEsco in #125

Allow automatic respecting of padding in layouts

The problem

When laying out a view, the parent ContourLayout's padding values are not respected. Aligning a subview to parent.top() will place the view at the top of the parent, even if the parent has a non-zero top padding. Similarly, constraining a view to parent.width() or parent.height() will constrain the view to the parent's total width and height, ignoring the padding.

Open questions

  1. Should respecting the padding be an overridable behaviour? (I'm not sure)
  2. Should respecting the padding be the default behaviour? (I think so)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.