├── .github
└── workflows
│ ├── Check.yml
│ └── Release.yml
├── .gitignore
├── .idea
└── copyright
│ ├── Square.xml
│ └── profiles_settings.xml
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── RELEASING.md
├── build.gradle
├── contour-lint
├── build.gradle
├── gradle.properties
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── squareup
│ │ └── contour
│ │ └── lint
│ │ ├── ContourIssueRegistry.kt
│ │ └── NestedContourLayoutsDetector.kt
│ └── test
│ └── java
│ └── com
│ └── squareup
│ └── contour
│ └── lint
│ └── NestedContourLayoutsDetectorTest.kt
├── contour
├── build.gradle
├── gradle.properties
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ └── kotlin
│ │ └── com
│ │ └── squareup
│ │ └── contour
│ │ ├── Api.kt
│ │ ├── ContourLayout.kt
│ │ ├── Geometry.kt
│ │ ├── SizeMode.kt
│ │ ├── XFloat.kt
│ │ ├── XInt.kt
│ │ ├── YFloat.kt
│ │ ├── YInt.kt
│ │ ├── constraints
│ │ ├── Constraint.kt
│ │ ├── SizeConfig.kt
│ │ └── SizeConfigSmartLambdas.kt
│ │ ├── errors
│ │ └── CircularReferenceDetected.kt
│ │ ├── solvers
│ │ ├── AxisSolver.kt
│ │ ├── ComparisonResolver.kt
│ │ └── SimpleAxisSolver.kt
│ │ ├── utils
│ │ ├── ViewGroups.kt
│ │ └── XYIntUtils.kt
│ │ └── wrappers
│ │ ├── HasDimensions.kt
│ │ └── ParentGeometry.kt
│ └── test
│ └── kotlin
│ └── com
│ └── squareup
│ └── contour
│ ├── ContourSizeConfigTests.kt
│ ├── ContourTests.kt
│ ├── XFloatTest.kt
│ ├── XIntTest.kt
│ ├── YFloatTest.kt
│ ├── YIntTest.kt
│ ├── utils
│ ├── ContourTestHelpers.kt
│ └── FakeTextView.kt
│ └── wrappers
│ └── ParentGeometryTest.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── sample-app
├── build.gradle
├── gradle.properties
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── squareup
│ │ └── contour
│ │ └── sample
│ │ ├── CircularImageView.kt
│ │ ├── Colors.kt
│ │ ├── ExpandableBioCard1.kt
│ │ ├── ExpandableBioCard2.kt
│ │ ├── PushOnPressAnimator.kt
│ │ ├── SampleActivity.kt
│ │ ├── SampleView.kt
│ │ └── widget
│ │ └── PaddingAdjusterWidget.kt
│ └── res
│ └── drawable
│ ├── android_logo.xml
│ └── check_mark.xml
├── screenshots
├── crd.png
├── droidcon_talk_cover.png
├── runtime_layout_logic.gif
└── simple_demo.gif
└── settings.gradle
/.github/workflows/Check.yml:
--------------------------------------------------------------------------------
1 | name: Check pull request
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | push:
8 | branches:
9 | - master
10 | tags:
11 | - '*'
12 |
13 | jobs:
14 | macos-build:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout the repo
19 | uses: actions/checkout@v2
20 | - name: Build the repo
21 | run: ./gradlew clean build
22 |
23 | env:
24 | GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
25 |
--------------------------------------------------------------------------------
/.github/workflows/Release.yml:
--------------------------------------------------------------------------------
1 | name: Publish a release
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | tags:
8 | - '*'
9 |
10 | jobs:
11 | release:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout repo
16 | uses: actions/checkout@v2.3.4
17 |
18 | - name: Publish artifacts
19 | run: ./gradlew publish --no-daemon
20 | env:
21 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
22 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
23 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }}
24 |
25 | env:
26 | GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | local.properties
4 | **/.idea/*
5 | !/.idea/copyright/
6 |
7 | .DS_Store
8 | build
9 | /captures
10 | reports
--------------------------------------------------------------------------------
/.idea/copyright/Square.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 1.1.0 - _2021-03-02_
4 |
5 | * Fix `compareTo` methods for `YInt` and `YFloat`.
6 | * Avoid measure both axis when we don't have to.
7 | * Support for live preview in Android Studio.
8 | * `layoutBy()` return the view it's applied to. You now can do
9 |
10 | ```kotlin
11 | val titleText : TextView
12 |
13 | init {
14 | titleText = TextView(context).apply {
15 | text = "Not sure what the title is yet"
16 | setTextColor(0x0)
17 | }.layoutBy(
18 | leftTo { parent.left() + 16.dip },
19 | topTo { parent.top() + 16.dip })
20 | }
21 | ```
22 |
23 | * Update to Kotlin `1.4.30`.
24 |
25 | ## 1.0.0 - _2020-09-21_
26 |
27 | * Rename `applyLayout()` to `layoutBy()`.
28 | * Add utility axis solvers:
29 | * [`contourWidthMatchParent()`](https://github.com/cashapp/contour/blob/e45ab4a625dbf37ec419d864ef7692c3c7bb01c7/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt#L307) and [`contourHeightMatchParent()`](https://github.com/cashapp/contour/blob/e45ab4a625dbf37ec419d864ef7692c3c7bb01c7/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt#L336)
30 | * [`contourWidthWrapContent()`](https://github.com/cashapp/contour/blob/e45ab4a625dbf37ec419d864ef7692c3c7bb01c7/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt#L328) and [`contourHeightWrapContent()`](https://github.com/cashapp/contour/blob/e45ab4a625dbf37ec419d864ef7692c3c7bb01c7/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt#L357)
31 | * [`matchXTo()`](https://github.com/cashapp/contour/blob/e45ab4a625dbf37ec419d864ef7692c3c7bb01c7/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt#L564) and [`matchYTo()`](https://github.com/cashapp/contour/blob/e45ab4a625dbf37ec419d864ef7692c3c7bb01c7/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt#L592)
32 | * [`matchParentX()`](https://github.com/cashapp/contour/blob/e45ab4a625dbf37ec419d864ef7692c3c7bb01c7/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt#L578) and [`matchParentY()`](https://github.com/cashapp/contour/blob/e45ab4a625dbf37ec419d864ef7692c3c7bb01c7/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt#L606)
33 |
34 | ### Breaking changes (for 0.1.7)
35 | * Add support for padding. This can be disabled on a per-View basis using [`respectPadding = false`](https://github.com/cashapp/contour/blob/e45ab4a625dbf37ec419d864ef7692c3c7bb01c7/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt#L176).
36 | * Remove `onInitializeLayout()` in favor of laying Views in `init`.
37 |
38 | ## 0.1.7 - _2020-05-19_
39 |
40 | * Add `compareTo` operations to contour primitives.
41 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Contour [DEPRECATED]
2 |
3 | Contour is deprecated. It has been amazing. It is stable and probably still running in the wild.
4 | But its development has ceased. We encourage developers to favor [Jetpack Compose UI](https://developer.android.com/jetpack/compose).
5 |
6 | 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.
7 |
8 |
9 |
10 | ### Deprecating XML
11 |
12 | 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](https://developer.android.com/topic/libraries/data-binding) 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?
13 |
14 | - Change configuration based on screen size
15 | - Lazy loading elements of the UI.
16 | - A / B testing components at runtime
17 | - Dynamic theming
18 | - Add & remove components based on app state
19 |
20 | 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?
21 |
22 | - No findViewById - view declaration exists in scope.
23 | - No R.id.my_view ambiguity - Kotlin has namespacing & encapsulation!
24 | - Static typing!
25 | - Great IDE Support (No more laggy xml editor)
26 | - ViewHolders aren’t needed
27 | - Easier DI
28 |
29 | ## Usage
30 |
31 | 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:
32 |
33 | ```kotlin
34 | class BioView(context: Context) : ContourLayout(context) {
35 | private val avatar = ImageView(context).apply {
36 | scaleType = CENTER_CROP
37 | Picasso.get().load("https://i.imgur.com/ajdangY.jpg").into(this)
38 | }
39 |
40 | private val bio = TextView(context).apply {
41 | textSize = 16f
42 | text = "..."
43 | }
44 |
45 | init {
46 | avatar.layoutBy(
47 | x = leftTo { parent.left() }.widthOf { 60.xdip },
48 | y = topTo { parent.top() }.heightOf { 60.ydip }
49 | )
50 | bio.layoutBy(
51 | x = leftTo { avatar.right() + 16.xdip }.rightTo { parent.right() },
52 | y = topTo { parent.top() }
53 | )
54 | contourHeightOf { bio.bottom() }
55 | }
56 | }
57 | ```
58 |
59 | #### Runtime Layout Logic
60 |
61 | 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.
62 |
63 | ```kotlin
64 | bio.layoutBy(
65 | x = ...
66 | y = topTo {
67 | if (isSelected) parent.top() + 16.ydip
68 | else avatar.centerY() - bio.height() / 2
69 | }.heightOf {
70 | if (isSelected) bio.preferredHeight()
71 | else 48.ydip
72 | }
73 | )
74 | ```
75 |
76 | When paired with an animator, your runtime layout decisions can even act as keyframes for smoothly updating your layout.
77 |
78 | ```kotlin
79 | setOnClickListener {
80 | TransitionManager.beginDelayedTransition(this)
81 | isSelected = !isSelected
82 | requestLayout()
83 | }
84 | ```
85 |
86 | What does the end result of this look like?
87 |
88 | 
89 |
90 | #### Context-Aware API
91 | 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.
92 |
93 | 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:
94 |
95 | ```kotlin
96 | layoutBy(
97 | x = leftTo { name.left() }.leftTo { name.right() },
98 | y = topTo { name.bottom() }
99 | )
100 | ```
101 | will not compile.
102 |
103 | #### Axis Type Safety
104 |
105 | Contour makes heavy use of inline classes to provide axis type safety in layouts. What this means is,
106 |
107 | ```kotlin
108 | toLeftOf { view.top() }
109 | ```
110 |
111 | 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()`.
112 |
113 | Inline classes are a lightweight compile-time addition that allow this feature with minimal to no performance costs.
114 | https://kotlinlang.org/docs/reference/inline-classes.html
115 |
116 | ### Circular Reference Debugging
117 |
118 | Circular references are pretty easy to unintentionally introduce in any layout. To accidentally declare
119 |
120 | - `name.right` aligns to `note.left`
121 | - and `note.left` aligns to `name.right`
122 |
123 | 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.
124 |
125 |
126 |
127 |
128 |
129 | ## Comparison with Compose
130 |
131 | There is a lot of buzz and interest around writing views in code right now with the development of [Jetpack Compose](https://developer.android.com/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.
132 |
133 | The only similarity between Contour and Compose is that they both realize writing layouts in Kotlin is maximum cool.
134 |
135 | ## Releases
136 |
137 | ```groovy
138 | implementation "app.cash.contour:contour:1.1.0"
139 | ```
140 |
141 | Snapshots of the development version are available in [Sonatype's `snapshots` repository](https://oss.sonatype.org/content/repositories/snapshots/app/cash/contour/ ).
142 |
143 | ```groovy
144 | repositories {
145 | mavenCentral()
146 | maven {
147 | url 'https://oss.sonatype.org/content/repositories/snapshots/'
148 | }
149 | }
150 | ```
151 |
152 | ## License
153 |
154 | ```
155 | Copyright 2019 Square, Inc.
156 |
157 | Licensed under the Apache License, Version 2.0 (the "License");
158 | you may not use this file except in compliance with the License.
159 | You may obtain a copy of the License at
160 |
161 | http://www.apache.org/licenses/LICENSE-2.0
162 |
163 | Unless required by applicable law or agreed to in writing, software
164 | distributed under the License is distributed on an "AS IS" BASIS,
165 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
166 | See the License for the specific language governing permissions and
167 | limitations under the License.
168 | ```
169 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | Releasing
2 | =========
3 |
4 | ### Prerequisite: Sonatype (Maven Central) Account
5 |
6 | Create an account on the [Sonatype issues site][sonatype_issues]. Ask an existing publisher to open
7 | an issue requesting publishing permissions for `app.cash` projects.
8 |
9 | ### Prerequisite: GPG Keys
10 |
11 | Generate a GPG key (RSA, 4096 bit, 3650 day) expiry, or use an existing one. You should leave the
12 | password empty for this key.
13 |
14 | ```
15 | $ gpg --full-generate-key
16 | ```
17 |
18 | Upload the GPG keys to public servers:
19 |
20 | ```
21 | $ gpg --list-keys --keyid-format LONG
22 | /Users/johnbarber/.gnupg/pubring.kbx
23 | ------------------------------
24 | pub rsa4096/XXXXXXXXXXXXXXXX 2019-07-16 [SC] [expires: 2029-07-13]
25 | YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
26 | uid [ultimate] John Barber
27 | sub rsa4096/ZZZZZZZZZZZZZZZZ 2019-07-16 [E] [expires: 2029-07-13]
28 |
29 | $ gpg --send-keys --keyserver keyserver.ubuntu.com XXXXXXXXXXXXXXXX
30 | ```
31 |
32 | ### Prerequisite: Gradle Properties
33 |
34 | Define publishing properties in `~/.gradle/gradle.properties`:
35 |
36 | ```
37 | signing.keyId=1A2345F8
38 | signing.password=
39 | signing.secretKeyRingFile=/Users/jbarber/.gnupg/secring.gpg
40 | ```
41 |
42 | `signing.keyId` is the GPG key's ID. Get it with this:
43 |
44 | ```
45 | $ gpg --list-keys --keyid-format SHORT
46 | ```
47 |
48 | `signing.password` is the password for this key. This might be empty!
49 |
50 | `signing.secretKeyRingFile` is the absolute path for `secring.gpg`. You may need to export this
51 | file manually with the following command where `XXXXXXXX` is the `keyId` above:
52 |
53 | ```
54 | $ gpg --keyring secring.gpg --export-secret-key XXXXXXXX > ~/.gnupg/secring.gpg
55 | ```
56 |
57 |
58 | Cutting a Release
59 | -----------------
60 |
61 | 1. Update `CHANGELOG.md`.
62 |
63 | 2. Set versions:
64 |
65 | ```
66 | export RELEASE_VERSION=X.Y.Z
67 | export NEXT_VERSION=X.Y.Z-SNAPSHOT
68 | ```
69 |
70 | 3. Set environment variables with your [Sonatype credentials][sonatype_issues].
71 |
72 | ```
73 | export SONATYPE_NEXUS_USERNAME=johnbarber
74 | export SONATYPE_NEXUS_PASSWORD=`pbpaste`
75 | ```
76 |
77 | 4. Update, build, and upload:
78 |
79 | ```
80 | sed -i "" \
81 | "s/VERSION_NAME=.*/VERSION_NAME=$RELEASE_VERSION/g" \
82 | `find . -name "gradle.properties"`
83 | sed -i "" \
84 | "s/\"app.cash.contour:\([^\:]*\):[^\"]*\"/\"app.cash.contour:\1:$RELEASE_VERSION\"/g" \
85 | `find . -name "README.md"`
86 | ./gradlew clean uploadArchives
87 | ```
88 |
89 | 5. Visit [Sonatype Nexus][sonatype_nexus] to promote (close then release) the artifact. Or drop it
90 | if there is a problem!
91 |
92 | 6. Tag the release, prepare for the next one, and push to GitHub.
93 |
94 | ```
95 | git commit -am "Prepare for release $RELEASE_VERSION."
96 | git tag -a parent-$RELEASE_VERSION -m "Version $RELEASE_VERSION"
97 | sed -i "" \
98 | "s/VERSION_NAME=.*/VERSION_NAME=$NEXT_VERSION/g" \
99 | `find . -name "gradle.properties"`
100 | git commit -am "Prepare next development version."
101 | git push && git push --tags
102 | ```
103 |
104 | [sonatype_issues]: https://issues.sonatype.org/
105 | [sonatype_nexus]: https://oss.sonatype.org/
106 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.github.ben-manes.versions'
2 |
3 | buildscript {
4 | ext.versions = [
5 | 'minSdk': 21,
6 | 'targetSdk': 30,
7 | 'agp': '7.0.0',
8 | 'lint': '30.0.0', // = 23.0.0 + agp
9 | 'kotlin': '1.5.10',
10 | 'autoService': '1.0',
11 | ]
12 |
13 | ext.deps = [
14 | 'androidx': [
15 | 'appCompat': 'androidx.appcompat:appcompat:1.3.1',
16 | 'ktx': 'androidx.core:core-ktx:1.6.0',
17 | 'transition': 'androidx.transition:transition:1.4.1',
18 | ],
19 | 'picasso': 'com.squareup.picasso:picasso:2.8',
20 | 'lint': [
21 | 'api': "com.android.tools.lint:lint-api:${versions.lint}",
22 | 'lint': "com.android.tools.lint:lint:${versions.lint}",
23 | 'tests': "com.android.tools.lint:lint-tests:${versions.lint}",
24 | ],
25 | 'auto': [
26 | 'service': "com.google.auto.service:auto-service:${versions.autoService}",
27 | 'serviceAnnotations': "com.google.auto.service:auto-service-annotations:${versions.autoService}",
28 | ],
29 | 'junit': 'junit:junit:4.13.2',
30 | 'robolectric': 'org.robolectric:robolectric:4.6.1',
31 | 'truth': 'com.google.truth:truth:1.1.3',
32 | 'plugins': [
33 | 'agp': "com.android.tools.build:gradle:${versions.agp}",
34 | 'kotlin': "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
35 | ],
36 | ]
37 |
38 | dependencies {
39 | classpath deps.plugins.agp
40 | classpath deps.plugins.kotlin
41 | classpath 'com.github.ben-manes:gradle-versions-plugin:0.39.0'
42 | classpath 'com.vanniktech:gradle-maven-publish-plugin:0.17.0'
43 | }
44 |
45 | repositories {
46 | google()
47 | mavenCentral()
48 | gradlePluginPortal()
49 | }
50 | }
51 |
52 | subprojects {
53 | repositories {
54 | google()
55 | mavenCentral()
56 | }
57 |
58 | tasks.withType(JavaCompile).configureEach { task ->
59 | task.sourceCompatibility = JavaVersion.VERSION_1_8
60 | task.targetCompatibility = JavaVersion.VERSION_1_8
61 | }
62 |
63 | tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile).configureEach { task ->
64 | task.kotlinOptions {
65 | jvmTarget = "1.8"
66 | }
67 | }
68 | }
69 |
70 | task clean(type: Delete) {
71 | delete rootProject.buildDir
72 | }
73 |
74 | subprojects { project ->
75 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
76 | kotlinOptions {
77 | jvmTarget = "1.8"
78 | }
79 | }
80 | }
81 |
82 | tasks.wrapper {
83 | distributionType = Wrapper.DistributionType.BIN
84 | }
--------------------------------------------------------------------------------
/contour-lint/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 | apply plugin: 'org.jetbrains.kotlin.jvm'
3 | apply plugin: 'org.jetbrains.kotlin.kapt'
4 | apply plugin: 'com.android.lint'
5 |
6 | targetCompatibility = JavaVersion.VERSION_1_8
7 | sourceCompatibility = JavaVersion.VERSION_1_8
8 |
9 | dependencies {
10 | compileOnly deps.lint.api
11 | compileOnly deps.auto.serviceAnnotations
12 | kapt deps.auto.service
13 | testImplementation deps.junit
14 | testImplementation deps.lint.lint
15 | testImplementation deps.lint.tests
16 | }
--------------------------------------------------------------------------------
/contour-lint/gradle.properties:
--------------------------------------------------------------------------------
1 | # needed so that :contour:prepareLintJarForPublish can succeed
2 | # Remove when the bug described in https://issuetracker.google.com/issues/161727305 is fixed
3 | kotlin.stdlib.default.dependency=false
4 |
--------------------------------------------------------------------------------
/contour-lint/src/main/java/com/squareup/contour/lint/ContourIssueRegistry.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.lint
18 |
19 | import com.android.tools.lint.client.api.IssueRegistry
20 | import com.android.tools.lint.client.api.Vendor
21 | import com.android.tools.lint.detector.api.CURRENT_API
22 | import com.google.auto.service.AutoService
23 |
24 | @Suppress("UnstableApiUsage", "unused")
25 | @AutoService(value = [IssueRegistry::class])
26 | class ContourIssueRegistry : IssueRegistry() {
27 | override val issues = listOf(NestedContourLayoutsDetector.ISSUE)
28 |
29 | override val api = CURRENT_API
30 |
31 | /**
32 | * works with Studio 4.0 or later; see
33 | * [com.android.tools.lint.detector.api.describeApi]
34 | */
35 | override val minApi = 7
36 |
37 | override val vendor = Vendor(
38 | vendorName = "cashapp/contour",
39 | identifier = "app.cash.contour:contour:{version}",
40 | feedbackUrl = "https://github.com/cashapp/contour/issues",
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/contour-lint/src/main/java/com/squareup/contour/lint/NestedContourLayoutsDetector.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.lint
18 |
19 | import com.android.tools.lint.client.api.UElementHandler
20 | import com.android.tools.lint.detector.api.Category
21 | import com.android.tools.lint.detector.api.Detector
22 | import com.android.tools.lint.detector.api.Implementation
23 | import com.android.tools.lint.detector.api.Issue
24 | import com.android.tools.lint.detector.api.JavaContext
25 | import com.android.tools.lint.detector.api.Scope
26 | import com.android.tools.lint.detector.api.Severity.ERROR
27 | import com.intellij.psi.PsiClass
28 | import com.intellij.psi.PsiClassType
29 | import com.intellij.psi.PsiType
30 | import org.jetbrains.uast.UCallExpression
31 | import org.jetbrains.uast.UElement
32 | import org.jetbrains.uast.ULambdaExpression
33 | import org.jetbrains.uast.getContainingUClass
34 | import org.jetbrains.uast.getParentOfType
35 | import org.jetbrains.uast.getUCallExpression
36 |
37 | @Suppress("UnstableApiUsage")
38 | class NestedContourLayoutsDetector : Detector(), Detector.UastScanner {
39 |
40 | override fun getApplicableUastTypes(): List> =
41 | listOf(UCallExpression::class.java)
42 |
43 | override fun createUastHandler(context: JavaContext): UElementHandler {
44 | return object : UElementHandler() {
45 | override fun visitCallExpression(node: UCallExpression) {
46 | if (isContourLayoutFunction(node)) {
47 | check(context, node)
48 | }
49 | }
50 | }
51 | }
52 |
53 | private fun isContourLayoutFunction(node: UCallExpression): Boolean {
54 | if (node.methodName != "layoutBy" && node.methodName != "applyLayout") return false
55 | if (node.valueArgumentCount < 2) return false
56 |
57 | // Type resolution for kotlin classes doesn't work when lint
58 | // is run from the command line so node.resolve() will be null.
59 | val function = node.resolve()
60 | if (function == null) {
61 | // Skip the 0th param, which will be the type param ().
62 | val argType = { at: Int -> node.getArgumentForParameter(at)?.getExpressionType() }
63 | return isAxisSolver(argType(1)) && isAxisSolver(argType(2))
64 |
65 | } else if (function.containingClass?.qualifiedName == "com.squareup.contour.ContourLayout") {
66 | val params = function.parameters
67 | return params[1]?.name == "x" && params[2]?.name == "y"
68 | }
69 | return false
70 | }
71 |
72 | private fun isAxisSolver(type: PsiType?): Boolean {
73 | if (type == null) return false
74 | if (type.canonicalText == "com.squareup.contour.solvers.AxisSolver") return true
75 | for (superT in type.superTypes) {
76 | if (isAxisSolver(superT)) return true
77 | }
78 | return false
79 | }
80 |
81 | private fun check(context: JavaContext, node: UCallExpression) {
82 | val isInsideLambda = node.getParentOfType() != null
83 | val isInsideContourView = isContourView(context, node.getContainingUClass())
84 | if (!isInsideLambda || !isInsideContourView) {
85 | return
86 | }
87 |
88 | val methodReceiver = (node.getUCallExpression()?.receiverType as? PsiClassType)?.resolve()
89 | val containingView = node.getContainingUClass()!!
90 |
91 | if (!containingView.isEquivalentTo(methodReceiver)) {
92 | // Code example:
93 | // class BarView : ContourLayout
94 | // class FooView : ContourLayout {
95 | // val view = BarView(context).apply {
96 | // layout(x, y) <- ERROR! The receiver of this layout() call isn't FooView.
97 | // }
98 | // }
99 | val nestedContourView = methodReceiver?.name
100 | context.report(
101 | issue = ISSUE,
102 | scope = node,
103 | location = context.getNameLocation(node),
104 | message = "Calling `${node.methodName}()` on the wrong scope: `$nestedContourView` " +
105 | "instead of `${containingView.name}`. This will result in an infinite loop. " +
106 | "Consider using a lambda that offers `$nestedContourView` as an argument " +
107 | "(e.g., `also`, `let`) instead of a receiver (e.g., `with`, `apply`, `run`) or" +
108 | " moving this layout logic to `${containingView.name}`'s `init` block."
109 | )
110 | }
111 | }
112 |
113 | private fun isContourView(context: JavaContext, type: PsiClass?): Boolean {
114 | return context.evaluator.extendsClass(type, "com.squareup.contour.ContourLayout", true)
115 | }
116 |
117 | companion object {
118 | val ISSUE = Issue.create(
119 | id = "NestedContourLayouts",
120 | briefDescription = "Incorrectly nested ContourLayouts",
121 | explanation = """
122 | When a nested contour view is initialized and laid out using a lambda that offers \
123 | `this` as a receiver (e.g., Kotlin's `apply{}` and `run{}`), it's easy to accidentally \
124 | call `layoutBy()`/`applyLayout()` on the wrong scope. This lint flags these kinds of \
125 | errors. Here's an example:
126 |
127 | ```
128 | class FooView : ContourLayout {
129 | val view = BarView(context).apply {
130 | layoutBy(x, y) <- ERROR! The receiver of this call is BarView and not FooView!
131 | }
132 | }
133 | class BarView(context: Context) : ContourLayout
134 | ```
135 |
136 | The above example result in a StackOverflowException because `FooView` ends up laying \
137 | out itself rather than `BarView`.
138 | """.trimMargin(),
139 | category = Category.CORRECTNESS,
140 | severity = ERROR,
141 | priority = 5,
142 | implementation = Implementation(
143 | NestedContourLayoutsDetector::class.java,
144 | Scope.JAVA_FILE_SCOPE
145 | )
146 | )
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/contour-lint/src/test/java/com/squareup/contour/lint/NestedContourLayoutsDetectorTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.lint
18 |
19 | import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
20 | import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
21 | import com.android.tools.lint.checks.infrastructure.TestFile
22 | import com.android.tools.lint.checks.infrastructure.TestLintTask
23 | import org.junit.FixMethodOrder
24 | import org.junit.Test
25 | import org.junit.runners.MethodSorters.JVM
26 |
27 | @Suppress("UnstableApiUsage")
28 | @FixMethodOrder(JVM)
29 | class NestedContourLayoutsDetectorTest {
30 |
31 | private fun lint(source: TestFile): TestLintTask {
32 | return TestLintTask.lint().files(
33 | VIEW_STUB,
34 | AXIS_SOLVERS_STUB,
35 | CONTOUR_LAYOUT_STUB,
36 | source
37 | )
38 | }
39 |
40 | @Test fun `nested contour views with 'this' as the receiver`() {
41 | val source = """
42 | |package sample
43 | |
44 | |import android.content.Context
45 | |import com.squareup.contour.ContourLayout
46 | |
47 | |class FooView(context: Context) : ContourLayout(context) {
48 | | private val view1 = BarView(context).apply {
49 | | layoutBy(
50 | | x = matchParentX(),
51 | | y = matchParentY()
52 | | )
53 | | }
54 | | private val view2 = with(BarView(context)) {
55 | | layoutBy(
56 | | x = matchParentX(),
57 | | y = matchParentY()
58 | | )
59 | | }
60 | | private val view3 = BarView(context).run {
61 | | applyLayout(
62 | | x = matchParentX(),
63 | | y = matchParentY()
64 | | )
65 | | }
66 | |}
67 | |
68 | |class BarView(context: Context) : ContourLayout(context)
69 | """.trimMargin()
70 |
71 | lint(kotlin(source))
72 | .issues(NestedContourLayoutsDetector.ISSUE)
73 | .run()
74 | .expect(
75 | """
76 | |src/sample/FooView.kt:8: Error: Calling layoutBy() on the wrong scope: BarView instead of FooView. This will result in an infinite loop. Consider using a lambda that offers BarView as an argument (e.g., also, let) instead of a receiver (e.g., with, apply, run) or moving this layout logic to FooView's init block. [NestedContourLayouts]
77 | | layoutBy(
78 | | ~~~~~~~~
79 | |src/sample/FooView.kt:14: Error: Calling layoutBy() on the wrong scope: BarView instead of FooView. This will result in an infinite loop. Consider using a lambda that offers BarView as an argument (e.g., also, let) instead of a receiver (e.g., with, apply, run) or moving this layout logic to FooView's init block. [NestedContourLayouts]
80 | | layoutBy(
81 | | ~~~~~~~~
82 | |src/sample/FooView.kt:20: Error: Calling applyLayout() on the wrong scope: BarView instead of FooView. This will result in an infinite loop. Consider using a lambda that offers BarView as an argument (e.g., also, let) instead of a receiver (e.g., with, apply, run) or moving this layout logic to FooView's init block. [NestedContourLayouts]
83 | | applyLayout(
84 | | ~~~~~~~~~~~
85 | |3 errors, 0 warnings
86 | """.trimMargin()
87 | )
88 | }
89 |
90 | @Test fun `nested contour views with 'this' as the argument`() {
91 | val source = """
92 | |package sample
93 | |
94 | |import android.content.Context
95 | |import com.squareup.contour.ContourLayout
96 | |
97 | |class FooView(context: Context) : ContourLayout(context) {
98 | | private val view1 = BarView(context).let {
99 | | layoutBy(
100 | | x = leftTo { parent.left() },
101 | | y = topTo { parent.top() }
102 | | )
103 | | }
104 | |
105 | | private val view2 = BarView(context).also {
106 | | layoutBy(
107 | | x = rightTo { parent.right() },
108 | | y = bottomTo { parent.bottom() }
109 | | )
110 | | }
111 | |}
112 | |
113 | |class BarView(context: Context) : ContourLayout(context)
114 | """.trimMargin()
115 |
116 | lint(kotlin(source))
117 | .issues(NestedContourLayoutsDetector.ISSUE)
118 | .run()
119 | .expectClean()
120 | }
121 |
122 | @Test fun `nested non-contour views`() {
123 | val source = """
124 | |package sample
125 | |
126 | |import android.content.Context
127 | |import android.view.View
128 | |import com.squareup.contour.ContourLayout
129 | |
130 | |class FooView(context: Context) : ContourLayout(context) {
131 | | private val view = View(context).apply {
132 | | layoutBy(
133 | | x = leftTo { parent.left() },
134 | | y = bottomTo { parent.bottom() }
135 | | )
136 | | }
137 | |}
138 | """.trimMargin()
139 |
140 | lint(kotlin(source))
141 | .issues(NestedContourLayoutsDetector.ISSUE)
142 | .run()
143 | .expectClean()
144 | }
145 |
146 | @Test fun `calling layoutBy outside a lambda`() {
147 | val source = """
148 | |package sample
149 | |
150 | |import android.content.Context
151 | |import android.view.View
152 | |import com.squareup.contour.ContourLayout
153 | |
154 | |class FooView(context: Context) : ContourLayout(context) {
155 | | private val view1 = View(context)
156 | | private val view2 = BarView(context)
157 | |
158 | | init {
159 | | view1.layoutBy(
160 | | x = leftTo { parent.left() },
161 | | y = topTo { parent.top() }
162 | | )
163 | |
164 | | view2.layoutBy(
165 | | x = rightTo { parent.right() },
166 | | y = bottomTo { parent.bottom() }
167 | | )
168 | | }
169 | |}
170 | |
171 | |class BarView(context: Context) : ContourLayout(context)
172 | """.trimMargin()
173 |
174 | lint(kotlin(source))
175 | .issues(NestedContourLayoutsDetector.ISSUE)
176 | .run()
177 | .expectClean()
178 | }
179 |
180 | @Test fun `nested contour views with complex class hierarchy`() {
181 | val source = """
182 | |package sample
183 | |
184 | |import android.content.Context
185 | |import com.squareup.contour.ContourLayout
186 | |
187 | |class FooView(context: Context) : OnClickListener, ContourLayout(context) {
188 | | private val view = BarBarView(context).apply {
189 | | layoutBy(
190 | | x = matchParentX(),
191 | | y = matchParentY()
192 | | )
193 | | }
194 | |}
195 | |
196 | |class BarBarView(context: Context) : OnClickListener, BarView(context)
197 | |open class BarView(context: Context) : ContourLayout(context)
198 | |interface OnClickListener
199 | """.trimMargin()
200 |
201 | lint(kotlin(source))
202 | .issues(NestedContourLayoutsDetector.ISSUE)
203 | .run()
204 | .expectErrorCount(1)
205 | }
206 |
207 | companion object {
208 | private val AXIS_SOLVERS_STUB = kotlin(
209 | """
210 | |package com.squareup.contour.solvers
211 | |
212 | |interface AxisSolver
213 | |interface XAxisSolver : AxisSolver
214 | |interface YAxisSolver : AxisSolver
215 | """.trimMargin()
216 | )
217 | private val CONTOUR_LAYOUT_STUB = kotlin(
218 | """
219 | |package com.squareup.contour
220 | |
221 | |import android.content.Context
222 | |import android.view.View
223 | |import android.view.ViewGroup
224 | |import com.squareup.contour.solvers.XAxisSolver
225 | |import com.squareup.contour.solvers.YAxisSolver
226 | |
227 | |open class ContourLayout(context: Context) : ViewGroup(context) {
228 | | fun View.layoutBy(x: XAxisSolver, y: YAxisSolver): Unit = TODO()
229 | | fun T.layoutBy(addToViewGroup: Boolean, spec: T.() -> LayoutSpec): T = TODO()
230 | | fun T.applyLayout(x: XAxisSolver, y: YAxisSolver): T = TODO()
231 | |}
232 | """.trimMargin()
233 | )
234 | private val VIEW_STUB = java(
235 | """
236 | |package android.view;
237 | |
238 | |import android.content.Context;
239 | |
240 | |public class View {
241 | | public View(Context context) {}
242 | |}
243 | """.trimMargin()
244 | )
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/contour/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.vanniktech.maven.publish'
4 |
5 | android {
6 | compileSdkVersion versions.targetSdk
7 |
8 | defaultConfig {
9 | minSdkVersion versions.minSdk
10 | }
11 |
12 | testOptions {
13 | unitTests {
14 | includeAndroidResources = true
15 | }
16 | }
17 | }
18 |
19 | dependencies {
20 | testImplementation deps.junit
21 | testImplementation deps.truth
22 | testImplementation deps.robolectric
23 |
24 | lintPublish project(':contour-lint')
25 | }
--------------------------------------------------------------------------------
/contour/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=contour
2 | POM_NAME=Contour
3 | POM_PACKAGING=aar
4 |
--------------------------------------------------------------------------------
/contour/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/Api.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 | @file:Suppress("unused")
18 |
19 | package com.squareup.contour
20 |
21 | import com.squareup.contour.SizeMode.Exact
22 | import com.squareup.contour.solvers.XAxisSolver
23 | import com.squareup.contour.solvers.YAxisSolver
24 |
25 | interface LayoutContainer {
26 | val parent: Geometry
27 | }
28 |
29 | interface WidthOfOnlyContext : XAxisSolver, HasXPositionWithoutWidth
30 | interface HeightOfOnlyContext : YAxisSolver, HasYPositionWithoutHeight
31 |
32 | interface HasXPositionWithoutWidth {
33 | fun widthOf(
34 | mode: SizeMode = Exact,
35 | provider: LayoutContainer.() -> XInt
36 | ): XAxisSolver
37 |
38 | fun widthOfFloat(
39 | mode: SizeMode = Exact,
40 | provider: LayoutContainer.() -> XFloat
41 | ): XAxisSolver
42 | }
43 |
44 | interface HasYPositionWithoutHeight {
45 | fun heightOf(
46 | mode: SizeMode = Exact,
47 | provider: LayoutContainer.() -> YInt
48 | ): YAxisSolver
49 |
50 | fun heightOfFloat(
51 | mode: SizeMode = Exact,
52 | provider: LayoutContainer.() -> YFloat
53 | ): YAxisSolver
54 | }
55 |
56 | interface HasLeft : XAxisSolver, HasXPositionWithoutWidth {
57 | fun rightTo(
58 | mode: SizeMode = Exact,
59 | provider: LayoutContainer.() -> XInt
60 | ): XAxisSolver
61 |
62 | fun rightToFloat(
63 | mode: SizeMode = Exact,
64 | provider: LayoutContainer.() -> XFloat
65 | ): XAxisSolver
66 | }
67 |
68 | interface HasRight : XAxisSolver, HasXPositionWithoutWidth {
69 | fun leftTo(
70 | mode: SizeMode = Exact,
71 | provider: LayoutContainer.() -> XInt
72 | ): XAxisSolver
73 |
74 | fun leftToFloat(
75 | mode: SizeMode = Exact,
76 | provider: LayoutContainer.() -> XFloat
77 | ): XAxisSolver
78 | }
79 |
80 | interface HasTop : YAxisSolver, HasYPositionWithoutHeight {
81 | fun bottomTo(
82 | mode: SizeMode = Exact,
83 | provider: LayoutContainer.() -> YInt
84 | ): YAxisSolver
85 |
86 | fun bottomToFloat(
87 | mode: SizeMode = Exact,
88 | provider: LayoutContainer.() -> YFloat
89 | ): YAxisSolver
90 | }
91 |
92 | interface HasBottom : YAxisSolver, HasYPositionWithoutHeight {
93 | fun topTo(
94 | mode: SizeMode = Exact,
95 | provider: LayoutContainer.() -> YInt
96 | ): YAxisSolver
97 |
98 | fun topToFloat(
99 | mode: SizeMode = Exact,
100 | provider: LayoutContainer.() -> YFloat
101 | ): YAxisSolver
102 | }
103 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/ContourLayout.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 | @file:Suppress("unused", "NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate")
18 |
19 | package com.squareup.contour
20 |
21 | import android.content.Context
22 | import android.graphics.Rect
23 | import android.util.AttributeSet
24 | import android.view.View
25 | import android.view.ViewGroup
26 | import com.squareup.contour.constraints.SizeConfig
27 | import com.squareup.contour.constraints.SizeConfigSmartLambdas.CoordinateAxis.HORIZONTAL
28 | import com.squareup.contour.constraints.SizeConfigSmartLambdas.CoordinateAxis.VERTICAL
29 | import com.squareup.contour.constraints.SizeConfigSmartLambdas.matchParent
30 | import com.squareup.contour.constraints.SizeConfigSmartLambdas.wrapContent
31 | import com.squareup.contour.errors.CircularReferenceDetected
32 | import com.squareup.contour.solvers.AxisSolver
33 | import com.squareup.contour.solvers.ComparisonResolver
34 | import com.squareup.contour.solvers.ComparisonResolver.CompareBy.MaxOf
35 | import com.squareup.contour.solvers.ComparisonResolver.CompareBy.MinOf
36 | import com.squareup.contour.solvers.SimpleAxisSolver
37 | import com.squareup.contour.solvers.SimpleAxisSolver.Point.Baseline
38 | import com.squareup.contour.solvers.SimpleAxisSolver.Point.Max
39 | import com.squareup.contour.solvers.SimpleAxisSolver.Point.Mid
40 | import com.squareup.contour.solvers.SimpleAxisSolver.Point.Min
41 | import com.squareup.contour.solvers.XAxisSolver
42 | import com.squareup.contour.solvers.YAxisSolver
43 | import com.squareup.contour.utils.toXInt
44 | import com.squareup.contour.utils.toYInt
45 | import com.squareup.contour.utils.unwrapXIntLambda
46 | import com.squareup.contour.utils.unwrapXIntToXIntLambda
47 | import com.squareup.contour.utils.unwrapYIntLambda
48 | import com.squareup.contour.utils.unwrapYIntToYIntLambda
49 | import com.squareup.contour.wrappers.HasDimensions
50 | import com.squareup.contour.wrappers.ParentGeometry
51 | import com.squareup.contour.wrappers.ViewDimensions
52 | import kotlin.math.max
53 | import kotlin.math.min
54 |
55 | private const val WRAP = ViewGroup.LayoutParams.WRAP_CONTENT
56 |
57 | /**
58 | * The central class to use when interacting with Contour.
59 | *
60 | * To build a custom layout inherit from [ContourLayout]. This will:
61 | * a) Expose a set of functions & extension functions - scoped to your layout - that will allow you to define
62 | * your layout.
63 | * b) Provide the layout mechanism to drive your layout.
64 | *
65 | * Views ~can~ be defined in XML layouts and configured with [ContourLayout] but this is not really recommended.
66 | * The intended use case for inheritors of [ContourLayout] is to define the children views programmatically in Kotlin
67 | * using [apply] block's to declare the views content and style. You can see examples of this in
68 | * [sample-app/src/main/java/com/squareup/contour/sample] but here is an example of defining a simple [TextView]:
69 | *
70 | * private val starDate = TextView(context).apply {
71 | * text = "Stardate: 23634.1"
72 | * setTextColor(White)
73 | * setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18f)
74 | * }
75 | *
76 | * This will instantiate and configure your child view but does not add it to your layout. To add a view to the
77 | * [ContourLayout] use the extension function [View.layoutBy] which is defined in the scope of [ContourLayout].
78 | * [View.layoutBy] does two things. It adds the child view to your layout - if not already added - and configures
79 | * the layout of the view. The configuration happens via the arguments provided to [View.layoutBy] which are an
80 | * instance of a [XAxisSolver] and a [YAxisSolver]. [XAxisSolver] and [YAxisSolver] are symmetrical interfaces which tell
81 | * Contour how to layout the child view on its x and y axes.
82 | *
83 | * To start defining [XAxisSolver] / [YAxisSolver] use any of the position declaration functions defined in the
84 | * [ContourLayout] scope. Eg: [leftTo], [rightTo], [centerHorizontallyTo], [topTo], etc. These functions will
85 | * return a [XAxisSolver]/[YAxisSolver] with the minimum configuration to show your view on screen.
86 | *
87 | * In the example above the view [starDate] could be configured with the code:
88 | *
89 | * starDate.layoutBy(
90 | * x = leftTo { parent.left() },
91 | * y = topTo { parent.top() }
92 | * )
93 | *
94 | * This would layout your view at the top-left corner of your layout with child views default desired size. In the case
95 | * of a [TextView] this would be to wrap it's text, in the case of an [ImageView] this would be the image resources
96 | * implicit size, in the case of the base class [View] this would be 0 width and height.
97 | *
98 | * In the example above there a couple things to note. First the reference to [parent] of made available within all
99 | * [XAxisSolver] / [YAxisSolver] scopes. [parent] represents the parent geometry and is guaranteed to be resolved when
100 | * any of its methods are called. The values returned by [parent] will be in the layuot's coordinate space, where
101 | * 0,0 is top-left and the layout width, height will be bottom-right.
102 | *
103 | * An additional thing to note is all the [parent] methods return one of two types: [XInt] and [YInt].
104 | * [XInt] and [YInt] represent a resolved layout value on it's corresponding axis, what this provides us is axis level
105 | * type-safety. For example:
106 | *
107 | * starDate.layoutBy(
108 | * x = leftTo { parent.top() },
109 | * y = topTo { parent.top() }
110 | * )
111 | *
112 | * Will not compile. [leftTo] requires an [XInt] and [top()] returns a [YInt]. The intention of this decision is to
113 | * a) avoid accidentally configure against an point on the wrong axis.
114 | * b) avoid accidentally referencing the virtual methods on [View] - [View.getLeft], [View.getTop], etc.
115 | * The methods [View.getLeft], [View.getTop], etc. still work and return [Int] values in the context of contour, but
116 | * do not provide the guarantee that the corresponding contour methods [View.left], [View.top], etc provide - which is
117 | * they will always be laid out and valid by the time the function returns.
118 | *
119 | * [XInt] and [YInt] are implemented as inline classes which means performance should be no different from using
120 | * regular [Int]s - and infact will compile down to native java [Int] primitives in most cases.
121 | * More on inline classes: https://kotlinlang.org/docs/reference/inline-classes.html
122 | *
123 | * In addition to siding [XAxisSolver] and [YAxisSolver] can define the width / height of the layout. In the example
124 | * above we can hard-code a height and set a max width with:
125 | *
126 | * starDate.layoutBy(
127 | * x = leftTo { parent.left() }
128 | * .rightTo(AtMost) { parent.right() },
129 | * y = topTo { parent.top() }
130 | * .heightOf { 50.ydip }
131 | * )
132 | *
133 | * In the above example width is implicitly determined by the leftTo, rightTo directives and by including the AtMost
134 | * argument, will dynamically size the width (starDate will be as wide as the text dictates - up until the right edge
135 | * reaches the parent.right() - when it will ellipsize). heightOf is simply hardcoded with heightOf { 50.ydip }.
136 | * Something of note here is the scoped extension function [ydip] and the corresponding [xdip]. These unsurprisingly
137 | * return Android DIP values as [XInt]/[YInt].
138 | *
139 | * You can also use an scoped extension function plain-old [dip], which cannot be used directly with Resolvers
140 | * ( widthOf { 10.dip } will not compile Int != YInt), but when combined with other functions doesn't require the
141 | * explicitness of [xdip]/[ydip] eg: ( widthOf { parent.width() - 10.dip } *will* compile. (XInt + Int) == XInt)
142 | *
143 | * Finally, you can reference not only your parent in contour, but any sibling in the layout. So instead of aligning
144 | * your left side with the [ContourLayout], you could instead reference another view, we'll call avatar:
145 | *
146 | * starDate.layoutBy(
147 | * x = leftTo { avatar.right() },
148 | * y = centerVerticallyTo { avatar.centerY() }
149 | * )
150 | *
151 | * Where you call [layoutBy] is up to you. There are two available styles. Either calling directly in your child
152 | * views [apply] block, or alternatively in your layout's [init] block. One thing to note about [layoutBy] is it
153 | * will add the view to the [ContourLayout] if not already added. This dictates the draw order - first added
154 | * will be drawn underneath everything else.
155 | */
156 | open class ContourLayout(
157 | context: Context,
158 | attrs: AttributeSet?
159 | ) : ViewGroup(context, attrs) {
160 |
161 | // Using a secondary constructor instead of @JvmOverloads ensures that
162 | // it shows up in autocomplete suggestions when extending ContourLayout.
163 | constructor(context: Context): this(context, null)
164 |
165 | /**
166 | * Whether Contour should respect padding set on this layout as part of laying out its subviews.
167 | * When set to `true`, padding will influence the alignment of subviews to the layout's inner edges
168 | * When set to `false`, padding will be ignored in all layout calculations.
169 | *
170 | * This flag exists for the sole purpose of facilitating baby-steps migration from alpha versions
171 | * of Contour where padding was not taken into account, allowing devs to migrate one view at a time.
172 | *
173 | * @see ViewGroup.setPadding
174 | */
175 | @Deprecated("Migrate implementation to properly take padding into account or explicitly null it out")
176 | var respectPadding: Boolean = true
177 |
178 | private val density: Float = context.resources.displayMetrics.density
179 | private val widthConfig = SizeConfig(lambda = matchParent())
180 | private val heightConfig = SizeConfig(lambda = matchParent())
181 | private val geometry = ParentGeometry(
182 | widthConfig = widthConfig,
183 | heightConfig = heightConfig,
184 | paddingConfig = {
185 | if (respectPadding) {
186 | Rect(paddingLeft, paddingTop, paddingRight, paddingBottom)
187 | } else {
188 | Rect(0, 0, 0, 0)
189 | }
190 | }
191 | )
192 | private var constructed: Boolean = true
193 | private var lastWidthSpec: Int = 0
194 | private var lastHeightSpec: Int = 0
195 |
196 | override fun requestLayout() {
197 | if (constructed) {
198 | invalidateAll()
199 | }
200 | super.requestLayout()
201 | }
202 |
203 | override fun onMeasure(
204 | widthMeasureSpec: Int,
205 | heightMeasureSpec: Int
206 | ) {
207 | if (lastWidthSpec != widthMeasureSpec || lastHeightSpec != heightMeasureSpec) {
208 | invalidateAll()
209 | }
210 |
211 | widthConfig.available = MeasureSpec.getSize(widthMeasureSpec)
212 | heightConfig.available = MeasureSpec.getSize(heightMeasureSpec)
213 | setMeasuredDimension(widthConfig.resolve(), heightConfig.resolve())
214 |
215 | lastWidthSpec = widthMeasureSpec
216 | lastHeightSpec = heightMeasureSpec
217 | }
218 |
219 | override fun onLayout(
220 | changed: Boolean,
221 | l: Int,
222 | t: Int,
223 | r: Int,
224 | b: Int
225 | ) {
226 | for (i in 0 until childCount) {
227 | val child = getChildAt(i)
228 | if (child.visibility == GONE) {
229 | continue
230 | }
231 |
232 | val params = child.spec()
233 | params.measureSelf()
234 | child.layout(
235 | params.left().value, params.top().value,
236 | params.right().value, params.bottom().value
237 | )
238 | }
239 | }
240 |
241 | private fun invalidateAll() {
242 | widthConfig.clear()
243 | heightConfig.clear()
244 | for (i in 0 until childCount) {
245 | val child = getChildAt(i)
246 | (child.layoutParams as? LayoutSpec)?.clear()
247 | }
248 | }
249 |
250 | private inline fun View.spec(): LayoutSpec {
251 | if (parent !== this@ContourLayout) {
252 | throw IllegalArgumentException("Referencing view outside of ViewGroup.")
253 | }
254 | return layoutParams as LayoutSpec
255 | }
256 |
257 | private inline fun View.handleCrd(block: () -> T): T {
258 | return try {
259 | block()
260 | } catch (e: CircularReferenceDetected) {
261 | // Want first stacktrace element to be following line.
262 | // Thread.currentThread().stackTrace does not do this.
263 | val trace = Throwable().stackTrace
264 | val current = trace.getOrNull(0)
265 | val calledBy = trace.getOrNull(1)
266 | e.add(CircularReferenceDetected.TraceElement(this, current, calledBy))
267 | throw e
268 | }
269 | }
270 |
271 | // API
272 |
273 | val Int.dip: Int get() = (density * this).toInt()
274 | val Int.xdip: XInt get() = XInt((density * this).toInt())
275 | val Int.ydip: YInt get() = YInt((density * this).toInt())
276 |
277 | val Float.dip: Float get() = density * this
278 | val Float.xdip: XFloat get() = XFloat(density * this)
279 | val Float.ydip: YFloat get() = YFloat(density * this)
280 |
281 | inline fun Int.toXInt(): XInt = XInt(this)
282 | inline fun Int.toYInt(): YInt = YInt(this)
283 |
284 | @Deprecated(
285 | message = "Views should be configured using layoutBy{} instead.",
286 | replaceWith = ReplaceWith("layoutBy(addToViewGroup, config)")
287 | )
288 | fun T.contourOf(
289 | addToViewGroup: Boolean = true,
290 | config: T.() -> LayoutSpec
291 | ): T {
292 | val viewGroup = this@ContourLayout
293 | val spec = config()
294 | spec.dimen = ViewDimensions(this)
295 | spec.parent = viewGroup.geometry
296 | layoutParams = spec
297 | if (addToViewGroup) {
298 | viewGroup.addViewInternal(this)
299 | }
300 | return this
301 | }
302 |
303 | /**
304 | * Overrides how the [ContourLayout] should size its width by reverting to the default behaviour, which
305 | * is to take all the available space it is given
306 | */
307 | fun contourWidthMatchParent() {
308 | widthConfig.lambda = matchParent()
309 | }
310 |
311 | /**
312 | * Overrides how the [ContourLayout] should size its width. By default [ContourLayout] will take all the available
313 | * space it is given. This override allows fine grained control over the resulting final measure.
314 | * @param config a function that takes a [XInt] - which is the available space supplied by the [ContourLayout]'s
315 | * parent - and returns a [XInt] describing how wide the [ContourLayout] should be.
316 | *
317 | * Note: It is acceptable to reference the children views in the [config] so long as circular references are not
318 | * introduced!
319 | */
320 | fun contourWidthOf(config: (available: XInt) -> XInt) {
321 | widthConfig.lambda = unwrapXIntToXIntLambda(config)
322 | }
323 |
324 | /**
325 | * Overrides how the [ContourLayout] should size its width. This override instructs the view to take up
326 | * as much space as necessary to wrap its subviews, including its own padding.
327 | */
328 | fun contourWidthWrapContent() {
329 | widthConfig.lambda = wrapContent(this, HORIZONTAL)
330 | }
331 |
332 | /**
333 | * Overrides how the [ContourLayout] should size its height by reverting to the default behaviour, which
334 | * is to take all the available space it is given
335 | */
336 | fun contourHeightMatchParent() {
337 | heightConfig.lambda = matchParent()
338 | }
339 |
340 | /**
341 | * Overrides how the [ContourLayout] should size its height. By default [ContourLayout] will take all the available
342 | * space it is given. This override allows fine grained control over the resulting final measure.
343 | * @param config a function that takes a [YInt] - which is the available space supplied by the [ContourLayout]'s
344 | * parent - and returns a [YInt] describing how tall the [ContourLayout] should be.
345 | *
346 | * Note: It is acceptable to reference the children views in the [config] so long as circular references are not
347 | * introduced!
348 | */
349 | fun contourHeightOf(config: (available: YInt) -> YInt) {
350 | heightConfig.lambda = unwrapYIntToYIntLambda(config)
351 | }
352 |
353 | /**
354 | * Overrides how the [ContourLayout] should size its height. This override instructs the view to take up
355 | * as much space as necessary to wrap its subviews, including its own padding.
356 | */
357 | fun contourHeightWrapContent() {
358 | heightConfig.lambda = wrapContent(this, VERTICAL)
359 | }
360 |
361 | /**
362 | * Describes a View's position and dimensions according to the [x] and [y] layout specs.
363 | * If needed, the layout spec can later be modified using [updateLayoutBy].
364 | *
365 | * Usage:
366 | *
367 | * ```
368 | * init {
369 | * starDate.layoutBy(
370 | * x = leftTo { parent.left() },
371 | * y = topTo { parent.top() }
372 | * )
373 | * }
374 | * ```
375 | *
376 | * @param addToViewGroup if true, this View will be add to the [ContourLayout] if it
377 | * is not already added.
378 | */
379 | fun T.layoutBy(
380 | x: XAxisSolver,
381 | y: YAxisSolver,
382 | addToViewGroup: Boolean = true
383 | ): T {
384 | val viewGroup = this@ContourLayout
385 | layoutParams = LayoutSpec(x, y).also {
386 | it.dimen = ViewDimensions(this)
387 | it.parent = viewGroup.geometry
388 | it.view = this
389 | }
390 | if (addToViewGroup && parent == null) {
391 | viewGroup.addViewInternal(this)
392 | }
393 | return this
394 | }
395 |
396 | @Deprecated(
397 | message = "Views should be configured using layoutBy() instead.",
398 | replaceWith = ReplaceWith("layoutBy(x, y, addToViewGroup)")
399 | )
400 | fun T.applyLayout(
401 | x: XAxisSolver,
402 | y: YAxisSolver,
403 | addToViewGroup: Boolean = true
404 | ) {
405 | layoutBy(x, y, addToViewGroup)
406 | }
407 |
408 | @Deprecated(message = "Use layoutBy(x = emptyX(), y = emptyY()) instead.")
409 | @Suppress("DeprecatedCallableAddReplaceWith") // breaks code
410 | fun View.applyEmptyLayout() =
411 | layoutBy(x = emptyX(), y = emptyY())
412 |
413 | /**
414 | * Updates the layout configuration of receiver view with new optional [XAxisSolver] and/or [YAxisSolver]
415 | * @receiver the view to configure
416 | * @param x configures how the [View] will be positioned and sized on the x-axis.
417 | * @param y configures how the [View] will be positioned and sized on the y-axis.
418 | */
419 | fun View.updateLayoutBy(
420 | x: XAxisSolver = spec().x,
421 | y: YAxisSolver = spec().y
422 | ) {
423 | val viewGroup = this@ContourLayout
424 | val spec = LayoutSpec(x, y)
425 | spec.dimen = ViewDimensions(this)
426 | spec.parent = viewGroup.geometry
427 | spec.view = this
428 | layoutParams = spec
429 | }
430 |
431 | @Deprecated("Use updateLayoutBy", ReplaceWith("updateLayoutBy(x, y)"))
432 | fun View.updateLayout(
433 | x: XAxisSolver = spec().x,
434 | y: YAxisSolver = spec().y
435 | ) {
436 | updateLayoutBy(x, y)
437 | }
438 |
439 | @Deprecated("Use updateLayoutBy", ReplaceWith("updateLayoutBy(x, y)"))
440 | fun View.updateLayoutSpec(
441 | x: XAxisSolver = spec().x,
442 | y: YAxisSolver = spec().y
443 | ) {
444 | updateLayoutBy(x, y)
445 | }
446 |
447 | /**
448 | * The left position of the receiver [View]. Guaranteed to return the resolved value or throw.
449 | * @return the laid-out left position of the [View]
450 | */
451 | fun View.left(): XInt = handleCrd { spec().left() }
452 |
453 | /**
454 | * The top position of the receiver [View]. Guaranteed to return the resolved value or throw.
455 | * @return the laid-out top position of the [View]
456 | */
457 | fun View.top(): YInt = handleCrd { spec().top() }
458 |
459 | /**
460 | * The right position of the receiver [View]. Guaranteed to return the resolved value or throw.
461 | * @return the laid-out right position of the [View]
462 | */
463 | fun View.right(): XInt = handleCrd { spec().right() }
464 |
465 | /**
466 | * The bottom position of the receiver [View]. Guaranteed to return the resolved value or throw.
467 | * @return the laid-out bottom position of the [View]
468 | */
469 | fun View.bottom(): YInt = handleCrd { spec().bottom() }
470 |
471 | /**
472 | * The center-x position of the receiver [View]. Guaranteed to return the resolved value or throw.
473 | * @return the laid-out left center-x of the [View]
474 | */
475 | fun View.centerX(): XInt = handleCrd { spec().centerX() }
476 |
477 | /**
478 | * The center-y position of the receiver [View]. Guaranteed to return the resolved value or throw.
479 | * @return the laid-out center-y position of the [View]
480 | */
481 | fun View.centerY(): YInt = handleCrd { spec().centerY() }
482 |
483 | /**
484 | * The baseline position of the receiver [View]. Guaranteed to return the resolved value or throw.
485 | * @return the laid-out baseline position of the [View]
486 | *
487 | * The baseline position will be 0 if the receiver [View] does not have a baseline. The most notable use of baseline
488 | * is in [TextView] which provides the baseline of the text.
489 | */
490 | fun View.baseline(): YInt = handleCrd { spec().baseline() }
491 |
492 | /**
493 | * The width of the receiver [View]. Guaranteed to return the resolved value or throw.
494 | * @return the laid-out width of the [View]
495 | */
496 | fun View.width(): XInt = handleCrd { spec().width() }
497 |
498 | /**
499 | * The height of the receiver [View]. Guaranteed to return the resolved value or throw.
500 | * @return the laid-out height of the [View]
501 | */
502 | fun View.height(): YInt = handleCrd { spec().height() }
503 |
504 | /**
505 | * The preferred width of the receiver [View] when no constraints are applied to the view.
506 | * @return the preferred width of the [View]
507 | */
508 | fun View.preferredWidth(): XInt = handleCrd { spec().preferredWidth() }
509 |
510 | /**
511 | * The preferred height of the receiver [View] when no constraints are applied to the view.
512 | * @return the preferred height of the [View]
513 | */
514 | fun View.preferredHeight(): YInt = handleCrd { spec().preferredHeight() }
515 |
516 | fun baselineTo(provider: LayoutContainer.() -> YInt): HeightOfOnlyContext =
517 | SimpleAxisSolver(
518 | point = Baseline,
519 | lambda = unwrapYIntLambda(provider)
520 | )
521 |
522 | fun topTo(provider: LayoutContainer.() -> YInt): HasTop =
523 | SimpleAxisSolver(
524 | point = Min,
525 | lambda = unwrapYIntLambda(provider)
526 | )
527 |
528 | fun bottomTo(provider: LayoutContainer.() -> YInt): HasBottom =
529 | SimpleAxisSolver(
530 | point = Max,
531 | lambda = unwrapYIntLambda(provider)
532 | )
533 |
534 | fun centerVerticallyTo(provider: LayoutContainer.() -> YInt): HeightOfOnlyContext =
535 | SimpleAxisSolver(
536 | point = Mid,
537 | lambda = unwrapYIntLambda(provider)
538 | )
539 |
540 | fun leftTo(provider: LayoutContainer.() -> XInt): HasLeft =
541 | SimpleAxisSolver(
542 | point = Min,
543 | lambda = unwrapXIntLambda(provider)
544 | )
545 |
546 | fun rightTo(provider: LayoutContainer.() -> XInt): HasRight =
547 | SimpleAxisSolver(
548 | point = Max,
549 | lambda = unwrapXIntLambda(provider)
550 | )
551 |
552 | fun centerHorizontallyTo(provider: LayoutContainer.() -> XInt): WidthOfOnlyContext =
553 | SimpleAxisSolver(
554 | point = Mid,
555 | lambda = unwrapXIntLambda(provider)
556 | )
557 |
558 | /**
559 | * Matches the position and width to those of [View] on the x-axis.
560 | * @param view sibling [View] to match to.
561 | * @param marginLeft extra space on the left.
562 | * @param marginRight extra space on the right.
563 | * @return the configured position on the x-axis
564 | */
565 | fun matchXTo(
566 | view: View,
567 | marginLeft: Int = 0,
568 | marginRight: Int = 0
569 | ): XAxisSolver {
570 | return leftTo { view.left() + marginLeft }.rightTo { view.right() - marginRight }
571 | }
572 |
573 | /**
574 | * Matches the position and width to those of the parent view on the x-axis.
575 | * @param marginLeft extra space on the left.
576 | * @param marginRight extra space on the right.
577 | * @return the configured position on the x-axis
578 | */
579 | fun matchParentX(
580 | marginLeft: Int = 0,
581 | marginRight: Int = 0
582 | ): XAxisSolver {
583 | return leftTo { parent.left() + marginLeft }.rightTo { parent.right() - marginRight }
584 | }
585 |
586 | /**
587 | * Matches the position and height to those of [View] on the y-axis.
588 | * @param view sibling [View] to match to.
589 | * @param marginTop extra space on the top.
590 | * @param marginBottom extra space on the bottom.
591 | * @return the configured position on the y-axis
592 | */
593 | fun matchYTo(
594 | view: View,
595 | marginTop: Int = 0,
596 | marginBottom: Int = 0
597 | ): YAxisSolver {
598 | return topTo { view.top() + marginTop }.bottomTo { view.bottom() - marginBottom }
599 | }
600 |
601 | /**
602 | * Matches the position and height to those of the parent view on the y-axis.
603 | * @param marginTop extra space on the top.
604 | * @param marginBottom extra space on the bottom.
605 | * @return the configured position on the y-axis
606 | */
607 | fun matchParentY(
608 | marginTop: Int = 0,
609 | marginBottom: Int = 0
610 | ): YAxisSolver {
611 | return topTo { parent.top() + marginTop }.bottomTo { parent.bottom() - marginBottom }
612 | }
613 |
614 | /**
615 | * Assigns zero-width to the view.
616 | */
617 | fun emptyX() = leftTo { parent.left() }.widthOf { 0.toXInt() }
618 |
619 | /**
620 | * Assigns zero-height to the view.
621 | */
622 | fun emptyY() = topTo { parent.top() }.heightOf { 0.toYInt() }
623 |
624 | fun minOf(
625 | a: XInt,
626 | b: XInt
627 | ): XInt = min(a.value, b.value).toXInt()
628 |
629 | fun minOf(
630 | a: YInt,
631 | b: YInt
632 | ): YInt = min(a.value, b.value).toYInt()
633 |
634 | fun maxOf(
635 | a: XInt,
636 | b: XInt
637 | ): XInt = max(a.value, b.value).toXInt()
638 |
639 | fun maxOf(
640 | a: YInt,
641 | b: YInt
642 | ): YInt = max(a.value, b.value).toYInt()
643 |
644 | fun minOf(
645 | p0: HasYPositionWithoutHeight,
646 | p1: HasYPositionWithoutHeight
647 | ): YAxisSolver {
648 | p0 as AxisSolver
649 | p1 as AxisSolver
650 | return ComparisonResolver(p0, p1, MinOf)
651 | }
652 |
653 | fun maxOf(
654 | p0: HasYPositionWithoutHeight,
655 | p1: HasYPositionWithoutHeight
656 | ): YAxisSolver {
657 | p0 as AxisSolver
658 | p1 as AxisSolver
659 | return ComparisonResolver(p0, p1, MaxOf)
660 | }
661 |
662 | fun minOf(
663 | p0: HasXPositionWithoutWidth,
664 | p1: HasXPositionWithoutWidth
665 | ): XAxisSolver {
666 | p0 as AxisSolver
667 | p1 as AxisSolver
668 | return ComparisonResolver(p0, p1, MinOf)
669 | }
670 |
671 | fun maxOf(
672 | p0: HasXPositionWithoutWidth,
673 | p1: HasXPositionWithoutWidth
674 | ): XAxisSolver {
675 | p0 as AxisSolver
676 | p1 as AxisSolver
677 | return ComparisonResolver(p0, p1, MaxOf)
678 | }
679 |
680 | private fun addViewInternal(child: View?) = super.addView(child)
681 |
682 | @Deprecated(ADDVIEW_DEPRECATION_MESSAGE, ReplaceWith(ADDVIEW_DEPRECATION_QUICKFIX))
683 | override fun addView(child: View) = super.addView(child)
684 |
685 | @Deprecated(ADDVIEW_DEPRECATION_MESSAGE, ReplaceWith(ADDVIEW_DEPRECATION_QUICKFIX))
686 | override fun addView(child: View, index: Int) = super.addView(child, index)
687 |
688 | @Deprecated(ADDVIEW_DEPRECATION_MESSAGE, ReplaceWith(ADDVIEW_DEPRECATION_QUICKFIX))
689 | override fun addView(child: View, params: LayoutParams) = super.addView(child, params)
690 |
691 | @Deprecated(ADDVIEW_DEPRECATION_MESSAGE, ReplaceWith(ADDVIEW_DEPRECATION_QUICKFIX))
692 | override fun addView(child: View, index: Int, params: LayoutParams) =
693 | super.addView(child, index, params)
694 |
695 | @Deprecated(ADDVIEW_DEPRECATION_MESSAGE, ReplaceWith(ADDVIEW_DEPRECATION_QUICKFIX))
696 | override fun addView(child: View, width: Int, height: Int) =
697 | super.addView(child, width, height)
698 |
699 | @Deprecated(ADDVIEW_DEPRECATION_MESSAGE, ReplaceWith(ADDVIEW_DEPRECATION_QUICKFIX))
700 | override fun addViewInLayout(child: View, index: Int, params: LayoutParams) =
701 | super.addViewInLayout(child, index, params)
702 |
703 | @Deprecated(ADDVIEW_DEPRECATION_MESSAGE, ReplaceWith(ADDVIEW_DEPRECATION_QUICKFIX))
704 | override fun addViewInLayout(
705 | child: View, index: Int, params: LayoutParams, preventRequestLayout: Boolean
706 | ) = if (child.layoutParams is LayoutSpec)
707 | super.addViewInLayout(child, index, params, preventRequestLayout)
708 | else
709 | throw UnsupportedOperationException(ADDVIEW_DEPRECATION_MESSAGE)
710 |
711 | class LayoutSpec(
712 | internal val x: XAxisSolver,
713 | internal val y: YAxisSolver
714 | ) : ViewGroup.LayoutParams(WRAP, WRAP), LayoutContainer {
715 |
716 | override lateinit var parent: Geometry
717 | internal lateinit var dimen: HasDimensions
718 | internal lateinit var view: View
719 |
720 | init {
721 | x.onAttach(this)
722 | y.onAttach(this)
723 | }
724 |
725 | internal fun left(): XInt = x.min().toXInt()
726 | internal fun right(): XInt = x.max().toXInt()
727 | internal fun centerX(): XInt = x.mid().toXInt()
728 | internal fun top(): YInt = y.min().toYInt()
729 | internal fun bottom(): YInt = y.max().toYInt()
730 | internal fun centerY(): YInt = y.mid().toYInt()
731 | internal fun baseline(): YInt = y.baseline().toYInt()
732 | internal fun width(): XInt = x.range().toXInt()
733 | internal fun height(): YInt = y.range().toYInt()
734 |
735 | internal fun preferredWidth(): XInt {
736 | return if (view.visibility == GONE) {
737 | XInt.ZERO
738 | } else {
739 | dimen.measure(0, y.measureSpec())
740 | dimen.width.toXInt()
741 | }
742 | }
743 |
744 | internal fun preferredHeight(): YInt {
745 | return if (view.visibility == GONE) {
746 | YInt.ZERO
747 | } else {
748 | dimen.measure(x.measureSpec(), 0)
749 | dimen.height.toYInt()
750 | }
751 | }
752 |
753 | internal fun measureSelf() {
754 | if (view.visibility == GONE) {
755 | x.onRangeResolved(0, 0)
756 | y.onRangeResolved(0, 0)
757 | } else {
758 | dimen.measure(x.measureSpec(), y.measureSpec())
759 | x.onRangeResolved(dimen.width, 0)
760 | y.onRangeResolved(dimen.height, dimen.baseline)
761 | }
762 | }
763 |
764 | internal fun clear() {
765 | x.clear()
766 | y.clear()
767 | }
768 | }
769 |
770 | companion object {
771 | const val ADDVIEW_DEPRECATION_MESSAGE = "Incorrectly adding view to ContourLayout"
772 | const val ADDVIEW_DEPRECATION_QUICKFIX = "child.layoutBy(x = matchParentX(), y = matchParentY())"
773 | }
774 | }
775 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/Geometry.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 | @file:Suppress("unused")
18 |
19 | package com.squareup.contour
20 |
21 | import android.graphics.Rect
22 |
23 | /**
24 | * Represents a rectangle in your layout.
25 | * The methods obey a contract that they will either return the correct laid out or throw a
26 | * [com.squareup.contour.errors.CircularReferenceDetected] if the [com.squareup.contour.ContourLayout]
27 | * is configured incorrectly.
28 | */
29 | interface Geometry {
30 | fun left(): XInt
31 | fun right(): XInt
32 | fun width(): XInt
33 | fun centerX(): XInt
34 |
35 | fun top(): YInt
36 | fun bottom(): YInt
37 | fun height(): YInt
38 | fun centerY(): YInt
39 |
40 | fun padding(): Rect
41 | }
42 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/SizeMode.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 com.squareup.contour
18 |
19 | import android.view.View.MeasureSpec
20 |
21 | /**
22 | * Corresponds with the [MeasureSpec] modes [MeasureSpec.EXACTLY] & [MeasureSpec.AT_MOST].
23 | * Majority of the time [SizeMode.Exact] is what should be used. A common use-case for [SizeMode.AtMost] is if you
24 | * are laying out text and want the corresponding view to size itself based on the text width, but ellipsize if the
25 | * once the view hits the specified maximum value.
26 | */
27 | enum class SizeMode(val mask: Int) {
28 | Exact(MeasureSpec.EXACTLY),
29 | AtMost(MeasureSpec.AT_MOST)
30 | }
31 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/XFloat.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 | @file:Suppress("EXPERIMENTAL_FEATURE_WARNING", "NOTHING_TO_INLINE", "unused")
18 |
19 | package com.squareup.contour
20 |
21 | /**
22 | * Represents an [Float] on the x axis.
23 | */
24 | @JvmInline
25 | value class XFloat(val value: Float) {
26 |
27 | inline operator fun plus(other: Int) = XFloat(value + other)
28 | inline operator fun plus(other: XInt) = XFloat(value + other.value)
29 | inline operator fun plus(other: Float) = XFloat(value + other)
30 | inline operator fun plus(other: XFloat) = XFloat(value + other.value)
31 |
32 | inline operator fun minus(other: Int) = XFloat(value - other)
33 | inline operator fun minus(other: XInt) = XFloat(value - other.value)
34 | inline operator fun minus(other: Float) = XFloat(value - other)
35 | inline operator fun minus(other: XFloat) = XFloat(value - other.value)
36 |
37 | inline operator fun times(other: Int) = XFloat(value * other)
38 | inline operator fun times(other: XInt) = XFloat(value * other.value)
39 | inline operator fun times(other: Float) = XFloat(value * other)
40 | inline operator fun times(other: XFloat) = XFloat(value * other.value)
41 |
42 | inline operator fun div(other: Int) = XFloat(value / other)
43 | inline operator fun div(other: XInt) = XFloat(value / other.value)
44 | inline operator fun div(other: Float) = XFloat(value / other)
45 | inline operator fun div(other: XFloat) = XFloat(value / other.value)
46 |
47 | inline operator fun compareTo(other: Int) = value.compareTo(other)
48 | inline operator fun compareTo(other: XInt) = value.compareTo(other.value)
49 | inline operator fun compareTo(other: Float) = value.compareTo(other)
50 | inline operator fun compareTo(other: XFloat) = value.compareTo(other.value)
51 |
52 | inline fun toY() = YFloat(value)
53 | inline fun toInt() = XInt(value.toInt())
54 |
55 | companion object {
56 | val ZERO = XFloat(0f)
57 | val MIN_VALUE = XFloat(Float.MIN_VALUE)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/XInt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 | @file:Suppress("EXPERIMENTAL_FEATURE_WARNING", "NOTHING_TO_INLINE", "unused")
18 |
19 | package com.squareup.contour
20 |
21 | /**
22 | * Represents an [Int] on the x axis.
23 | */
24 | @JvmInline
25 | value class XInt(val value: Int) {
26 |
27 | inline operator fun plus(other: Int) = XInt(value + other)
28 | inline operator fun plus(other: XInt) = XInt(value + other.value)
29 | inline operator fun plus(other: Float) = XFloat(value + other)
30 | inline operator fun plus(other: XFloat) = XFloat(value + other.value)
31 |
32 | inline operator fun minus(other: Int) = XInt(value - other)
33 | inline operator fun minus(other: XInt) = XInt(value - other.value)
34 | inline operator fun minus(other: Float) = XFloat(value - other)
35 | inline operator fun minus(other: XFloat) = XFloat(value - other.value)
36 |
37 | inline operator fun times(other: Int) = XInt(value * other)
38 | inline operator fun times(other: XInt) = XInt(value * other.value)
39 | inline operator fun times(other: Float) = XFloat(value * other)
40 | inline operator fun times(other: XFloat) = XFloat(value * other.value)
41 |
42 | inline operator fun div(other: Int) = XInt(value / other)
43 | inline operator fun div(other: XInt) = XInt(value / other.value)
44 | inline operator fun div(other: Float) = XFloat(value / other)
45 | inline operator fun div(other: XFloat) = XFloat(value / other.value)
46 |
47 | inline operator fun compareTo(other: Int) = value.compareTo(other)
48 | inline operator fun compareTo(other: XInt) = value.compareTo(other.value)
49 | inline operator fun compareTo(other: Float) = value.compareTo(other)
50 | inline operator fun compareTo(other: XFloat) = value.compareTo(other.value)
51 |
52 | inline fun toY() = YInt(value)
53 | inline fun toFloat() = XFloat(value.toFloat())
54 |
55 | companion object {
56 | val ZERO = XInt(0)
57 | val MIN_VALUE = XInt(Int.MIN_VALUE)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/YFloat.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 | @file:Suppress("EXPERIMENTAL_FEATURE_WARNING", "NOTHING_TO_INLINE", "unused")
18 |
19 | package com.squareup.contour
20 |
21 | /**
22 | * Represents an [Float] on the y axis.
23 | */
24 | @JvmInline
25 | value class YFloat(val value: Float) {
26 |
27 | inline operator fun plus(other: Int) = YFloat(value + other)
28 | inline operator fun plus(other: YInt) = YFloat(value + other.value)
29 | inline operator fun plus(other: Float) = YFloat(value + other)
30 | inline operator fun plus(other: YFloat) = YFloat(value + other.value)
31 |
32 | inline operator fun minus(other: Int) = YFloat(value - other)
33 | inline operator fun minus(other: YInt) = YFloat(value - other.value)
34 | inline operator fun minus(other: Float) = YFloat(value - other)
35 | inline operator fun minus(other: YFloat) = YFloat(value - other.value)
36 |
37 | inline operator fun times(other: Int) = YFloat(value * other)
38 | inline operator fun times(other: YInt) = YFloat(value * other.value)
39 | inline operator fun times(other: Float) = YFloat(value * other)
40 | inline operator fun times(other: YFloat) = YFloat(value * other.value)
41 |
42 | inline operator fun div(other: Int) = YFloat(value / other)
43 | inline operator fun div(other: YInt) = YFloat(value / other.value)
44 | inline operator fun div(other: Float) = YFloat(value / other)
45 | inline operator fun div(other: YFloat) = YFloat(value / other.value)
46 |
47 | inline operator fun compareTo(other: Int) = value.compareTo(other)
48 | inline operator fun compareTo(other: YInt) = value.compareTo(other.value)
49 | inline operator fun compareTo(other: Float) = value.compareTo(other)
50 | inline operator fun compareTo(other: YFloat) = value.compareTo(other.value)
51 |
52 | inline fun toX() = XFloat(value)
53 | inline fun toInt() = YInt(value.toInt())
54 |
55 | companion object {
56 | val ZERO = YFloat(0f)
57 | val MIN_VALUE = YFloat(Float.MIN_VALUE)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/YInt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 | @file:Suppress("EXPERIMENTAL_FEATURE_WARNING", "NOTHING_TO_INLINE", "unused")
18 |
19 | package com.squareup.contour
20 |
21 | /**
22 | * Represents an [Int] on the y axis.
23 | */
24 | @JvmInline
25 | value class YInt(val value: Int) {
26 |
27 | inline operator fun plus(other: Int) = YInt(value + other)
28 | inline operator fun plus(other: YInt) = YInt(value + other.value)
29 | inline operator fun plus(other: Float) = YFloat(value + other)
30 | inline operator fun plus(other: YFloat) = YFloat(value + other.value)
31 |
32 | inline operator fun minus(other: Int) = YInt(value - other)
33 | inline operator fun minus(other: YInt) = YInt(value - other.value)
34 | inline operator fun minus(other: Float) = YFloat(value - other)
35 | inline operator fun minus(other: YFloat) = YFloat(value - other.value)
36 |
37 | inline operator fun times(other: Int) = YInt(value * other)
38 | inline operator fun times(other: YInt) = YInt(value * other.value)
39 | inline operator fun times(other: Float) = YFloat(value * other)
40 | inline operator fun times(other: YFloat) = YFloat(value * other.value)
41 |
42 | inline operator fun div(other: Int) = YInt(value / other)
43 | inline operator fun div(other: YInt) = YInt(value / other.value)
44 | inline operator fun div(other: Float) = YFloat(value / other)
45 | inline operator fun div(other: YFloat) = YFloat(value / other.value)
46 |
47 | inline operator fun compareTo(other: Int) = value.compareTo(other)
48 | inline operator fun compareTo(other: YInt) = value.compareTo(other.value)
49 | inline operator fun compareTo(other: Float) = value.compareTo(other)
50 | inline operator fun compareTo(other: YFloat) = value.compareTo(other.value)
51 |
52 | inline fun toX() = XInt(value)
53 | inline fun toFloat() = YFloat(value.toFloat())
54 |
55 | companion object {
56 | val ZERO = YInt(0)
57 | val MIN_VALUE = YInt(Int.MIN_VALUE)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/constraints/Constraint.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 com.squareup.contour.constraints
18 |
19 | import com.squareup.contour.LayoutContainer
20 | import com.squareup.contour.SizeMode
21 | import com.squareup.contour.SizeMode.Exact
22 | import com.squareup.contour.errors.CircularReferenceDetected
23 | import com.squareup.contour.solvers.SimpleAxisSolver.Point
24 |
25 | internal open class Constraint {
26 | private var isResolving: Boolean = false
27 | private var container: LayoutContainer? = null
28 | private var value: Int = Int.MIN_VALUE
29 | var mode: SizeMode = Exact
30 | var lambda: (LayoutContainer.() -> Int)? = null
31 |
32 | val isSet: Boolean get() = lambda != null
33 |
34 | fun onAttachContext(container: LayoutContainer) {
35 | this.container = container
36 | }
37 |
38 | fun resolve(): Int {
39 | if (value == Int.MIN_VALUE) {
40 | val context =
41 | checkNotNull(container) { "Constraint called before LayoutContainer attached" }
42 | val lambda = checkNotNull(lambda) { "Constraint not set" }
43 |
44 | try {
45 | if (isResolving) throw CircularReferenceDetected()
46 |
47 | isResolving = true
48 | value = lambda(context)
49 | } finally {
50 | isResolving = false
51 | }
52 | }
53 | return value
54 | }
55 |
56 | fun clear() {
57 | value = Int.MIN_VALUE
58 | }
59 | }
60 |
61 | internal class PositionConstraint(
62 | var point: Point = Point.Min,
63 | lambda: (LayoutContainer.() -> Int)? = null
64 | ) : Constraint() {
65 | init {
66 | this.lambda = lambda
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/constraints/SizeConfig.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 com.squareup.contour.constraints
18 |
19 | internal typealias SizeConfigLambda = (available: Int) -> Int
20 | internal class SizeConfig(
21 | var available: Int = Int.MIN_VALUE,
22 | var result: Int = Int.MIN_VALUE,
23 | var lambda: SizeConfigLambda
24 | ) {
25 |
26 | fun resolve(): Int {
27 | if (result == Int.MIN_VALUE) {
28 | require(available != Int.MIN_VALUE) { "Triggering layout before parent geometry available" }
29 | result = lambda(available)
30 | }
31 | return result
32 | }
33 |
34 | fun clear() {
35 | result = Int.MIN_VALUE
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/constraints/SizeConfigSmartLambdas.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.constraints
18 |
19 | import android.view.View
20 | import com.squareup.contour.ContourLayout
21 | import com.squareup.contour.constraints.SizeConfigSmartLambdas.CoordinateAxis.HORIZONTAL
22 | import com.squareup.contour.constraints.SizeConfigSmartLambdas.CoordinateAxis.VERTICAL
23 | import com.squareup.contour.utils.children
24 | import kotlin.math.max
25 |
26 | internal object SizeConfigSmartLambdas {
27 | fun matchParent(): SizeConfigLambda = { it }
28 | fun wrapContent(view: ContourLayout, axis: CoordinateAxis): SizeConfigLambda = {
29 | view.run {
30 | children
31 | .filter { it.visibility != View.GONE }
32 | .map {
33 | when (axis) {
34 | VERTICAL -> max(it.bottom().value, paddingTop) + paddingBottom
35 | HORIZONTAL -> max(it.right().value, paddingLeft) + paddingRight
36 | }
37 | }
38 | .maxOrNull() ?: totalPadding(axis)
39 | }
40 | }
41 |
42 | private fun ContourLayout.totalPadding(axis: CoordinateAxis) = when (axis) {
43 | VERTICAL -> paddingTop + paddingBottom
44 | HORIZONTAL -> paddingLeft + paddingRight
45 | }
46 |
47 | enum class CoordinateAxis {
48 | VERTICAL,
49 | HORIZONTAL
50 | }
51 | }
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/errors/CircularReferenceDetected.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 com.squareup.contour.errors
18 |
19 | import android.view.View
20 |
21 | class CircularReferenceDetected : Exception() {
22 |
23 | class TraceElement(
24 | val view: View,
25 | val seenAt: StackTraceElement?,
26 | val referencedFrom: StackTraceElement?
27 | )
28 |
29 | private val list = ArrayList()
30 |
31 | fun add(trace: TraceElement) {
32 | list += trace
33 | }
34 |
35 | override val message: String
36 | get() = buildString {
37 | val count = list.size
38 | appendLine()
39 | appendLine()
40 | appendLine("Circular reference detected through the following calls:")
41 | list.forEachIndexed { i, t ->
42 | val bullet = "${count - i}) "
43 | val indent = " ".repeat(bullet.length)
44 | append(bullet).appendLine("Calling ${t.seenAt?.methodName}() on ${t.view} from:")
45 | append(indent).appendLine(t.referencedFrom.toString())
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/solvers/AxisSolver.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 com.squareup.contour.solvers
18 |
19 | import com.squareup.contour.ContourLayout.LayoutSpec
20 |
21 | /**
22 | * [AxisSolver] represents a strategy for solving points on an axis.
23 | * Implementations of this interface should be as lazy as possible. Invokers of this interface
24 | * use the least-knowledge fields as possible. This helps solve certain types of layouts where axis dimension and
25 | * position need to be calculated separately.
26 | *
27 | * Calling into the [min], [mid], [baseline], [max], [range] methods will trigger other [AxisSolver]s in your
28 | * layout configuration if this Solver is not fully resolved.
29 | *
30 | * Pairs of scalar resolvers create a bounding box for a laid-out view. The [XAxisSolver] computes the
31 | * left and right edges, and the [YAxisSolver] computes the top and bottom edges.
32 | *
33 | * The [YAxisSolver] may also compute the baseline.
34 | *
35 | * Most applications shouldn't need to implement this type.
36 | */
37 |
38 | interface AxisSolver {
39 |
40 | /**
41 | * Represents the left or top point of a component in pixels relative to the parent layout's top / left
42 | * Calling this method may trigger work.
43 | */
44 | fun min(): Int
45 |
46 | /**
47 | * Represents the center x or y point of a component in pixels relative to the parent layout's top / left
48 | * Calling this method may trigger work.
49 | */
50 | fun mid(): Int
51 |
52 | /**
53 | * Represents the text baseline of a component with text on the y axis relative parent layout's top.
54 | * Calling this method may trigger work.
55 | *
56 | * If component does not have text - or is representing the x axis this will resolve to 0. This will probably change.
57 | */
58 | fun baseline(): Int
59 |
60 | /**
61 | * Represents the right or bottom point of a component in pixels relative to the parent layout's top / left
62 | * Calling this method may trigger work.
63 | */
64 | fun max(): Int
65 |
66 | /**
67 | * Represents the width or height of a component in pixels. This may be solved without any of the axis point being
68 | * solved or vice-versa
69 | * Calling this method may trigger work.
70 | */
71 | fun range(): Int
72 |
73 | fun onAttach(parent: LayoutSpec)
74 | fun onRangeResolved(range: Int, baselineRange: Int)
75 |
76 | fun measureSpec(): Int
77 | fun clear()
78 | }
79 |
80 | interface XAxisSolver : AxisSolver
81 | interface YAxisSolver : AxisSolver
82 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/solvers/ComparisonResolver.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 com.squareup.contour.solvers
18 |
19 | import android.view.View
20 | import com.squareup.contour.constraints.Constraint
21 | import com.squareup.contour.ContourLayout.LayoutSpec
22 | import com.squareup.contour.solvers.ComparisonResolver.CompareBy.MaxOf
23 | import com.squareup.contour.solvers.ComparisonResolver.CompareBy.MinOf
24 |
25 | internal class ComparisonResolver(
26 | private val p0: AxisSolver,
27 | private val p1: AxisSolver,
28 | private val compareBy: CompareBy
29 | ) : XAxisSolver, YAxisSolver {
30 |
31 | internal enum class CompareBy {
32 | MaxOf,
33 | MinOf
34 | }
35 |
36 | private val size = Constraint()
37 | private var found: AxisSolver? = null
38 | private lateinit var parent: LayoutSpec
39 | private var range: Int = Int.MIN_VALUE
40 |
41 | private fun findWinner(): AxisSolver {
42 | val found = found
43 | if (found != null) return found
44 | else {
45 | when (compareBy) {
46 | MaxOf -> {
47 | val max = if (p0.min() >= p1.min()) p0 else p1
48 | this.found = max
49 | return max
50 | }
51 | MinOf -> {
52 | val min = if (p0.min() <= p1.min()) p0 else p1
53 | this.found = min
54 | return min
55 | }
56 | }
57 |
58 | }
59 | }
60 |
61 | override fun min(): Int = findWinner().min()
62 | override fun mid(): Int = findWinner().mid()
63 | override fun baseline(): Int = findWinner().baseline()
64 | override fun max(): Int = findWinner().max()
65 |
66 | override fun range(): Int {
67 | if (range == Int.MIN_VALUE) {
68 | parent.measureSelf()
69 | }
70 | return range
71 | }
72 |
73 | override fun onAttach(parent: LayoutSpec) {
74 | this.parent = parent
75 | p0.onAttach(parent)
76 | p1.onAttach(parent)
77 | }
78 |
79 | override fun onRangeResolved(range: Int, baselineRange: Int) {
80 | this.range = range
81 | p0.onRangeResolved(range, baselineRange)
82 | p1.onRangeResolved(range, baselineRange)
83 | }
84 |
85 | override fun measureSpec(): Int {
86 | return if (size.isSet) {
87 | View.MeasureSpec.makeMeasureSpec(size.resolve(), View.MeasureSpec.EXACTLY)
88 | } else {
89 | 0
90 | }
91 | }
92 |
93 | override fun clear() {
94 | p0.clear()
95 | p1.clear()
96 | found = null
97 | range = Int.MIN_VALUE
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/solvers/SimpleAxisSolver.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 com.squareup.contour.solvers
18 |
19 | import android.view.View
20 | import com.squareup.contour.ContourLayout.LayoutSpec
21 | import com.squareup.contour.HasBottom
22 | import com.squareup.contour.HasLeft
23 | import com.squareup.contour.HasRight
24 | import com.squareup.contour.HasTop
25 | import com.squareup.contour.HeightOfOnlyContext
26 | import com.squareup.contour.LayoutContainer
27 | import com.squareup.contour.SizeMode
28 | import com.squareup.contour.WidthOfOnlyContext
29 | import com.squareup.contour.XFloat
30 | import com.squareup.contour.XInt
31 | import com.squareup.contour.YFloat
32 | import com.squareup.contour.YInt
33 | import com.squareup.contour.constraints.Constraint
34 | import com.squareup.contour.constraints.PositionConstraint
35 | import com.squareup.contour.utils.unwrapXFloatLambda
36 | import com.squareup.contour.utils.unwrapXIntLambda
37 | import com.squareup.contour.utils.unwrapYFloatLambda
38 | import com.squareup.contour.utils.unwrapYIntLambda
39 | import kotlin.math.abs
40 |
41 | internal class SimpleAxisSolver(
42 | point: Point,
43 | lambda: LayoutContainer.() -> Int
44 | ) :
45 | XAxisSolver, HasLeft, HasRight, WidthOfOnlyContext,
46 | YAxisSolver, HasTop, HasBottom, HeightOfOnlyContext {
47 |
48 | internal enum class Point {
49 | Min,
50 | Mid,
51 | Baseline,
52 | Max
53 | }
54 |
55 | private lateinit var parent: LayoutSpec
56 |
57 | private val p0 = PositionConstraint(point, lambda)
58 | private val p1 = PositionConstraint()
59 | private val size = Constraint()
60 |
61 | private var min = Int.MIN_VALUE
62 | private var mid = Int.MIN_VALUE
63 | private var baseline = Int.MIN_VALUE
64 | private var max = Int.MIN_VALUE
65 |
66 | private var range = Int.MIN_VALUE
67 | private var baselineRange = Int.MIN_VALUE
68 |
69 | override fun min(): Int {
70 | if (min == Int.MIN_VALUE) {
71 | if (p0.point == Point.Min) {
72 | min = p0.resolve()
73 | } else {
74 | resolveRange()
75 | resolveAxis()
76 | }
77 | }
78 | return min
79 | }
80 |
81 | override fun mid(): Int {
82 | if (mid == Int.MIN_VALUE) {
83 | if (p0.point == Point.Mid) {
84 | mid = p0.resolve()
85 | } else {
86 | resolveRange()
87 | resolveAxis()
88 | }
89 | }
90 | return mid
91 | }
92 |
93 | override fun baseline(): Int {
94 | if (baseline == Int.MIN_VALUE) {
95 | if (p0.point == Point.Baseline) {
96 | baseline = p0.resolve()
97 | } else {
98 | if (baselineRange == Int.MIN_VALUE) {
99 | parent.measureSelf()
100 | } else {
101 | resolveRange()
102 | }
103 | resolveAxis()
104 | }
105 | }
106 | return baseline
107 | }
108 |
109 | override fun max(): Int {
110 | if (max == Int.MIN_VALUE) {
111 | if (p0.point == Point.Max) {
112 | max = p0.resolve()
113 | } else {
114 | resolveRange()
115 | resolveAxis()
116 | }
117 | }
118 | return max
119 | }
120 |
121 | override fun range(): Int {
122 | if (range == Int.MIN_VALUE) {
123 | resolveRange()
124 | }
125 | return range
126 | }
127 |
128 | private fun resolveRange() {
129 | if (parent.view.visibility == View.GONE) {
130 | onRangeResolved(0, 0)
131 | } else {
132 | if (p0.point == Point.Baseline && baselineRange == Int.MIN_VALUE) {
133 | parent.measureSelf()
134 | } else if (p1.isSet && p1.mode == SizeMode.Exact) {
135 | range = abs(p0.resolve() - p1.resolve())
136 | } else if (size.isSet && size.mode == SizeMode.Exact) {
137 | range = size.resolve()
138 | } else {
139 | parent.measureSelf()
140 | }
141 | }
142 | }
143 |
144 | private fun resolveAxis() {
145 | check(range != Int.MIN_VALUE)
146 |
147 | val hV = range / 2
148 | when (p0.point) {
149 | Point.Min -> {
150 | min = p0.resolve()
151 | mid = min + hV
152 | max = min + range
153 | }
154 | Point.Mid -> {
155 | mid = p0.resolve()
156 | min = mid - hV
157 | max = mid + hV
158 | }
159 | Point.Baseline -> {
160 | check(baselineRange != Int.MIN_VALUE)
161 | baseline = p0.resolve()
162 | min = baseline - baselineRange
163 | mid = min + hV
164 | max = min + range
165 | }
166 | Point.Max -> {
167 | max = p0.resolve()
168 | mid = max - hV
169 | min = max - range
170 | }
171 | }
172 | if (p0.point != Point.Baseline && baselineRange != Int.MIN_VALUE) {
173 | baseline = min + baselineRange
174 | }
175 | }
176 |
177 | override fun onAttach(parent: LayoutSpec) {
178 | this.parent = parent
179 | p0.onAttachContext(parent)
180 | p1.onAttachContext(parent)
181 | size.onAttachContext(parent)
182 | }
183 |
184 | override fun onRangeResolved(range: Int, baselineRange: Int) {
185 | this.range = range
186 | this.baselineRange = baselineRange
187 | }
188 |
189 | override fun measureSpec(): Int {
190 | return if (p1.isSet) {
191 | View.MeasureSpec.makeMeasureSpec(abs(p0.resolve() - p1.resolve()), p1.mode.mask)
192 | } else if (size.isSet) {
193 | View.MeasureSpec.makeMeasureSpec(size.resolve(), size.mode.mask)
194 | } else {
195 | 0
196 | }
197 | }
198 |
199 | override fun clear() {
200 | min = Int.MIN_VALUE
201 | mid = Int.MIN_VALUE
202 | baseline = Int.MIN_VALUE
203 | max = Int.MIN_VALUE
204 | range = Int.MIN_VALUE
205 | baselineRange = Int.MIN_VALUE
206 | p0.clear()
207 | p1.clear()
208 | size.clear()
209 | }
210 |
211 | override fun leftTo(
212 | mode: SizeMode,
213 | provider: LayoutContainer.() -> XInt
214 | ): XAxisSolver {
215 | p1.point = Point.Min
216 | p1.mode = mode
217 | p1.lambda = unwrapXIntLambda(provider)
218 | baselineRange = 0
219 | return this
220 | }
221 |
222 | override fun leftToFloat(
223 | mode: SizeMode,
224 | provider: LayoutContainer.() -> XFloat
225 | ): XAxisSolver {
226 | p1.point = Point.Min
227 | p1.mode = mode
228 | p1.lambda = unwrapXFloatLambda(provider)
229 | baselineRange = 0
230 | return this
231 | }
232 |
233 | override fun topTo(
234 | mode: SizeMode,
235 | provider: LayoutContainer.() -> YInt
236 | ): YAxisSolver {
237 | p1.point = Point.Min
238 | p1.mode = mode
239 | p1.lambda = unwrapYIntLambda(provider)
240 | return this
241 | }
242 |
243 | override fun topToFloat(
244 | mode: SizeMode,
245 | provider: LayoutContainer.() -> YFloat
246 | ): YAxisSolver {
247 | p1.point = Point.Min
248 | p1.mode = mode
249 | p1.lambda = unwrapYFloatLambda(provider)
250 | return this
251 | }
252 |
253 | override fun rightTo(mode: SizeMode, provider: LayoutContainer.() -> XInt): XAxisSolver {
254 | p1.point = Point.Max
255 | p1.mode = mode
256 | p1.lambda = unwrapXIntLambda(provider)
257 | baselineRange = 0
258 | return this
259 | }
260 |
261 | override fun rightToFloat(
262 | mode: SizeMode,
263 | provider: LayoutContainer.() -> XFloat
264 | ): XAxisSolver {
265 | p1.point = Point.Max
266 | p1.mode = mode
267 | p1.lambda = unwrapXFloatLambda(provider)
268 | baselineRange = 0
269 | return this
270 | }
271 |
272 | override fun bottomTo(
273 | mode: SizeMode,
274 | provider: LayoutContainer.() -> YInt
275 | ): YAxisSolver {
276 | p1.point = Point.Mid
277 | p1.mode = mode
278 | p1.lambda = unwrapYIntLambda(provider)
279 | return this
280 | }
281 |
282 | override fun bottomToFloat(
283 | mode: SizeMode,
284 | provider: LayoutContainer.() -> YFloat
285 | ): YAxisSolver {
286 | p1.point = Point.Mid
287 | p1.mode = mode
288 | p1.lambda = unwrapYFloatLambda(provider)
289 | return this
290 | }
291 |
292 | override fun widthOf(mode: SizeMode, provider: LayoutContainer.() -> XInt): XAxisSolver {
293 | size.mode = mode
294 | size.lambda = unwrapXIntLambda(provider)
295 | baselineRange = 0
296 | return this
297 | }
298 |
299 | override fun widthOfFloat(
300 | mode: SizeMode,
301 | provider: LayoutContainer.() -> XFloat
302 | ): XAxisSolver {
303 | size.mode = mode
304 | size.lambda = unwrapXFloatLambda(provider)
305 | baselineRange = 0
306 | return this
307 | }
308 |
309 | override fun heightOf(
310 | mode: SizeMode,
311 | provider: LayoutContainer.() -> YInt
312 | ): YAxisSolver {
313 | size.mode = mode
314 | size.lambda = unwrapYIntLambda(provider)
315 | return this
316 | }
317 |
318 | override fun heightOfFloat(
319 | mode: SizeMode,
320 | provider: LayoutContainer.() -> YFloat
321 | ): YAxisSolver {
322 | size.mode = mode
323 | size.lambda = unwrapYFloatLambda(provider)
324 | return this
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/utils/ViewGroups.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.utils
18 |
19 | import android.view.View
20 | import android.view.ViewGroup
21 |
22 | /**
23 | * Returns a [MutableIterator] over the views in this view group.
24 | * @note @carranca: This was pulled from ktx 1.3.0 so the whole lib didn't have to get pulled in.
25 | * */
26 | operator fun ViewGroup.iterator() = object : MutableIterator {
27 | private var index = 0
28 | override fun hasNext() = index < childCount
29 | override fun next() = getChildAt(index++) ?: throw IndexOutOfBoundsException()
30 | override fun remove() = removeViewAt(--index)
31 | }
32 |
33 | /**
34 | * Returns a [Sequence] over the child views in this view group.
35 | * @note @carranca: This was pulled from ktx 1.3.0 so the whole lib didn't have to get pulled in.
36 | * */
37 | val ViewGroup.children: Sequence
38 | get() = object : Sequence {
39 | override fun iterator() = this@children.iterator()
40 | }
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/utils/XYIntUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 | @file:Suppress("EXPERIMENTAL_FEATURE_WARNING", "NOTHING_TO_INLINE", "unused")
18 |
19 | package com.squareup.contour.utils
20 |
21 | import com.squareup.contour.LayoutContainer
22 | import com.squareup.contour.XFloat
23 | import com.squareup.contour.XInt
24 | import com.squareup.contour.YFloat
25 | import com.squareup.contour.YInt
26 | import com.squareup.contour.constraints.SizeConfigLambda
27 |
28 | internal inline fun unwrapXIntToXIntLambda(
29 | crossinline lambda: (XInt) -> XInt
30 | ): SizeConfigLambda =
31 | { lambda(it.toXInt()).value }
32 |
33 | internal inline fun unwrapYIntToYIntLambda(
34 | crossinline lambda: (YInt) -> YInt
35 | ): SizeConfigLambda =
36 | { lambda(it.toYInt()).value }
37 |
38 | internal inline fun unwrapXIntLambda(
39 | crossinline lambda: LayoutContainer.() -> XInt
40 | ): LayoutContainer.() -> Int =
41 | { lambda().value }
42 |
43 | internal inline fun unwrapXFloatLambda(
44 | crossinline lambda: LayoutContainer.() -> XFloat
45 | ): LayoutContainer.() -> Int =
46 | { lambda().value.toInt() }
47 |
48 | internal inline fun unwrapYIntLambda(
49 | crossinline lambda: LayoutContainer.() -> YInt
50 | ): LayoutContainer.() -> Int =
51 | { lambda().value }
52 |
53 | internal inline fun unwrapYFloatLambda(
54 | crossinline lambda: LayoutContainer.() -> YFloat
55 | ): LayoutContainer.() -> Int =
56 | { lambda().value.toInt() }
57 |
58 | internal inline fun Int.toXInt() = XInt(this)
59 | internal inline fun Int.toYInt() = YInt(this)
60 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/wrappers/HasDimensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 com.squareup.contour.wrappers
18 |
19 | import android.view.View
20 |
21 | internal interface HasDimensions {
22 | fun measure(
23 | w: Int,
24 | h: Int
25 | )
26 |
27 | val width: Int
28 | val height: Int
29 | val baseline: Int
30 | }
31 |
32 | internal class ViewDimensions(private val view: View) : HasDimensions {
33 | override fun measure(
34 | w: Int,
35 | h: Int
36 | ) {
37 | view.measure(w, h)
38 | }
39 |
40 | override val width: Int
41 | get() = view.measuredWidth
42 | override val height: Int
43 | get() = view.measuredHeight
44 | override val baseline: Int
45 | get() = view.baseline
46 | }
47 |
--------------------------------------------------------------------------------
/contour/src/main/kotlin/com/squareup/contour/wrappers/ParentGeometry.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 Square Inc.
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 com.squareup.contour.wrappers
18 |
19 | import android.graphics.Rect
20 | import com.squareup.contour.Geometry
21 | import com.squareup.contour.XInt
22 | import com.squareup.contour.YInt
23 | import com.squareup.contour.constraints.SizeConfig
24 | import com.squareup.contour.utils.toXInt
25 | import com.squareup.contour.utils.toYInt
26 |
27 | internal class ParentGeometry(
28 | private val widthConfig: SizeConfig,
29 | private val heightConfig: SizeConfig,
30 | private val paddingConfig: () -> Rect
31 | ) : Geometry {
32 | override fun left(): XInt = XInt.ZERO + padding().left
33 | override fun right(): XInt = widthConfig.resolve().toXInt() - padding().right
34 | override fun width(): XInt = widthConfig.resolve().toXInt()
35 | override fun centerX(): XInt = widthConfig.resolve().toXInt() / 2
36 |
37 | override fun top(): YInt = YInt.ZERO + padding().top
38 | override fun bottom(): YInt = heightConfig.resolve().toYInt() - padding().bottom
39 | override fun height(): YInt = heightConfig.resolve().toYInt()
40 | override fun centerY(): YInt = heightConfig.resolve().toYInt() / 2
41 |
42 | override fun padding(): Rect = paddingConfig()
43 | }
44 |
--------------------------------------------------------------------------------
/contour/src/test/kotlin/com/squareup/contour/ContourSizeConfigTests.kt:
--------------------------------------------------------------------------------
1 | package com.squareup.contour
2 |
3 | import android.app.Activity
4 | import android.view.View
5 | import android.view.View.GONE
6 | import android.view.View.INVISIBLE
7 | import android.view.View.VISIBLE
8 | import com.google.common.truth.Truth.assertThat
9 | import com.squareup.contour.utils.contourLayout
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.robolectric.Robolectric
13 | import org.robolectric.RobolectricTestRunner
14 |
15 | @RunWith(RobolectricTestRunner::class)
16 | class ContourSizeConfigTests {
17 | private val activity = Robolectric.buildActivity(Activity::class.java).get()
18 |
19 | @Test
20 | fun `match parent size config, no subviews`() {
21 | val layout = contourLayout(
22 | context = activity,
23 | width = 200,
24 | height = 50
25 | ) {
26 | setPadding(1, 2, 4, 8)
27 | contourWidthMatchParent()
28 | contourHeightMatchParent()
29 | }
30 |
31 | assertThat(layout.left).isEqualTo(0)
32 | assertThat(layout.top).isEqualTo(0)
33 | assertThat(layout.right).isEqualTo(200)
34 | assertThat(layout.bottom).isEqualTo(50)
35 | assertThat(layout.width).isEqualTo(200)
36 | assertThat(layout.height).isEqualTo(50)
37 | }
38 |
39 | @Test
40 | fun `match parent size config, multiple subviews`() {
41 | val view1 = View(activity).apply {
42 | visibility = VISIBLE
43 | }
44 | val view2 = View(activity).apply {
45 | visibility = VISIBLE
46 | }
47 | val layout = contourLayout(
48 | context = activity,
49 | width = 200,
50 | height = 50
51 | ) {
52 | setPadding(1, 2, 4, 8)
53 | contourWidthMatchParent()
54 | contourHeightMatchParent()
55 |
56 | view1.layoutBy(
57 | x = leftTo { parent.left() }.widthOf { 100.toXInt() },
58 | y = topTo { parent.top() }.heightOf { 25.toYInt() }
59 | )
60 |
61 | view2.layoutBy(
62 | x = leftTo { parent.left() }.widthOf { 201.toXInt() },
63 | y = topTo { parent.top() }.heightOf { 51.toYInt() }
64 | )
65 | }
66 |
67 | assertThat(layout.left).isEqualTo(0)
68 | assertThat(layout.top).isEqualTo(0)
69 | assertThat(layout.right).isEqualTo(200)
70 | assertThat(layout.bottom).isEqualTo(50)
71 | assertThat(layout.width).isEqualTo(200)
72 | assertThat(layout.height).isEqualTo(50)
73 | }
74 |
75 | @Test
76 | fun `wrap content size config, no subviews`() {
77 | val paddingLeft = 1
78 | val paddingTop = 2
79 | val paddingRight = 4
80 | val paddingBottom = 8
81 | val layout = contourLayout(
82 | context = activity,
83 | width = 200,
84 | height = 50
85 | ) {
86 | setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
87 | contourWidthWrapContent()
88 | contourHeightWrapContent()
89 | }
90 |
91 | assertThat(layout.left).isEqualTo(0)
92 | assertThat(layout.top).isEqualTo(0)
93 | assertThat(layout.right).isEqualTo(paddingLeft + paddingRight)
94 | assertThat(layout.bottom).isEqualTo(paddingTop + paddingBottom)
95 | assertThat(layout.width).isEqualTo(paddingLeft + paddingRight)
96 | assertThat(layout.height).isEqualTo(paddingTop + paddingBottom)
97 | }
98 |
99 | @Test
100 | fun `wrap content size config, multiple subviews`() {
101 | val view1 = View(activity).apply {
102 | visibility = VISIBLE
103 | }
104 | val view2 = View(activity).apply {
105 | visibility = VISIBLE
106 | }
107 | val invisibleView = View(activity).apply {
108 | visibility = INVISIBLE
109 | }
110 | val goneView = View(activity).apply {
111 | visibility = GONE
112 | }
113 |
114 | val paddingLeft = 1
115 | val paddingTop = 2
116 | val paddingRight = 4
117 | val paddingBottom = 8
118 | val layout = contourLayout(
119 | context = activity,
120 | width = 200,
121 | height = 50
122 | ) {
123 | setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
124 | contourWidthWrapContent()
125 | contourHeightWrapContent()
126 |
127 | view1.layoutBy(
128 | x = leftTo { parent.left() }.widthOf { 100.toXInt() },
129 | y = topTo { parent.top() }.heightOf { 25.toYInt() }
130 | )
131 |
132 | view2.layoutBy(
133 | x = leftTo { parent.left() }.widthOf { 200.toXInt() },
134 | y = topTo { parent.top() }.heightOf { 50.toYInt() }
135 | )
136 |
137 | invisibleView.layoutBy(
138 | x = leftTo { parent.left() }.widthOf { 300.toXInt() },
139 | y = topTo { parent.top() }.heightOf { 60.toYInt() }
140 | )
141 |
142 | goneView.layoutBy(
143 | x = leftTo { parent.left() }.widthOf { 400.toXInt() },
144 | y = topTo { view1.bottom() }.heightOf { 70.toYInt() }
145 | )
146 | }
147 |
148 | assertThat(layout.left).isEqualTo(0)
149 | assertThat(layout.top).isEqualTo(0)
150 | assertThat(layout.right).isEqualTo(paddingLeft + 300 + paddingRight)
151 | assertThat(layout.bottom).isEqualTo(paddingTop + 60 + paddingBottom)
152 | assertThat(layout.width).isEqualTo(paddingLeft + 300 + paddingRight)
153 | assertThat(layout.height).isEqualTo(paddingTop + 60 + paddingBottom)
154 | }
155 |
156 | @Test
157 | fun `exact size config, no subviews`() {
158 | val paddingLeft = 1
159 | val paddingTop = 2
160 | val paddingRight = 4
161 | val paddingBottom = 8
162 | val layout = contourLayout(
163 | context = activity,
164 | width = 200,
165 | height = 50
166 | ) {
167 | setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
168 | contourWidthOf { 16.toXInt() }
169 | contourHeightOf { 32.toYInt() }
170 | }
171 |
172 | assertThat(layout.left).isEqualTo(0)
173 | assertThat(layout.top).isEqualTo(0)
174 | assertThat(layout.right).isEqualTo(16)
175 | assertThat(layout.bottom).isEqualTo(32)
176 | assertThat(layout.width).isEqualTo(16)
177 | assertThat(layout.height).isEqualTo(32)
178 | }
179 |
180 | @Test
181 | fun `exact size config, multiple subviews`() {
182 | val view1 = View(activity).apply {
183 | visibility = VISIBLE
184 | }
185 | val view2 = View(activity).apply {
186 | visibility = VISIBLE
187 | }
188 | val invisibleView = View(activity).apply {
189 | visibility = INVISIBLE
190 | }
191 | val goneView = View(activity).apply {
192 | visibility = GONE
193 | }
194 |
195 | val paddingLeft = 1
196 | val paddingTop = 2
197 | val paddingRight = 4
198 | val paddingBottom = 8
199 | val layout = contourLayout(
200 | context = activity,
201 | width = 200,
202 | height = 50
203 | ) {
204 | setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
205 | contourWidthOf { 16.toXInt() }
206 | contourHeightOf { 32.toYInt() }
207 |
208 | view1.layoutBy(
209 | x = leftTo { parent.left() }.widthOf { 100.toXInt() },
210 | y = topTo { parent.top() }.heightOf { 25.toYInt() }
211 | )
212 |
213 | view2.layoutBy(
214 | x = leftTo { parent.left() }.widthOf { 200.toXInt() },
215 | y = topTo { parent.top() }.heightOf { 50.toYInt() }
216 | )
217 |
218 | invisibleView.layoutBy(
219 | x = leftTo { parent.left() }.widthOf { 300.toXInt() },
220 | y = topTo { parent.top() }.heightOf { 60.toYInt() }
221 | )
222 |
223 | goneView.layoutBy(
224 | x = leftTo { parent.left() }.widthOf { 400.toXInt() },
225 | y = topTo { view1.bottom() }.heightOf { 70.toYInt() }
226 | )
227 | }
228 |
229 | assertThat(layout.left).isEqualTo(0)
230 | assertThat(layout.top).isEqualTo(0)
231 | assertThat(layout.right).isEqualTo(16)
232 | assertThat(layout.bottom).isEqualTo(32)
233 | assertThat(layout.width).isEqualTo(16)
234 | assertThat(layout.height).isEqualTo(32)
235 | }
236 | }
--------------------------------------------------------------------------------
/contour/src/test/kotlin/com/squareup/contour/ContourTests.kt:
--------------------------------------------------------------------------------
1 | package com.squareup.contour
2 |
3 | import android.app.Activity
4 | import android.view.View
5 | import com.google.common.truth.Truth.assertThat
6 | import com.squareup.contour.utils.FakeTextView
7 | import com.squareup.contour.utils.contourLayout
8 | import com.squareup.contour.utils.forceRelayout
9 | import com.squareup.contour.utils.toXInt
10 | import com.squareup.contour.utils.toYInt
11 | import org.junit.Test
12 | import org.junit.runner.RunWith
13 | import org.robolectric.Robolectric
14 | import org.robolectric.RobolectricTestRunner
15 |
16 | @RunWith(RobolectricTestRunner::class)
17 | class ContourTests {
18 |
19 | private val activity = Robolectric.buildActivity(Activity::class.java).get()
20 |
21 | @Test
22 | fun `simple single child layout`() {
23 | val plainOldView = View(activity)
24 |
25 | contourLayout(
26 | context = activity,
27 | width = 200,
28 | height = 50
29 | ) {
30 | plainOldView.layoutBy(
31 | leftTo { parent.left() }.rightTo { parent.right() },
32 | topTo { parent.top() }.bottomTo { parent.bottom() }
33 | )
34 | }
35 |
36 | assertThat(plainOldView.left).isEqualTo(0)
37 | assertThat(plainOldView.top).isEqualTo(0)
38 | assertThat(plainOldView.right).isEqualTo(200)
39 | assertThat(plainOldView.bottom).isEqualTo(50)
40 | assertThat(plainOldView.width).isEqualTo(200)
41 | assertThat(plainOldView.height).isEqualTo(50)
42 | }
43 |
44 | @Test
45 | fun `simple layout, with padding`() {
46 | val plainOldView = View(activity)
47 | val centeredView = View(activity)
48 |
49 | val leftPadding = 1
50 | val topPadding = 2
51 | val rightPadding = 4
52 | val bottomPadding = 8
53 |
54 | contourLayout(
55 | context = activity,
56 | width = 200,
57 | height = 50
58 | ) {
59 | setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
60 | plainOldView.layoutBy(
61 | leftTo { parent.left() }.rightTo { parent.right() },
62 | topTo { parent.top() }.bottomTo { parent.bottom() }
63 | )
64 | centeredView.layoutBy(
65 | centerHorizontallyTo { parent.centerX() }.widthOf { 10.xdip },
66 | centerVerticallyTo { parent.centerY() }.heightOf { 10.ydip }
67 | )
68 | }
69 |
70 | assertThat(plainOldView.left).isEqualTo(leftPadding)
71 | assertThat(plainOldView.top).isEqualTo(topPadding)
72 | assertThat(plainOldView.right).isEqualTo(200 - rightPadding)
73 | assertThat(plainOldView.bottom).isEqualTo(50 - bottomPadding)
74 | assertThat(plainOldView.width).isEqualTo(200 - leftPadding - rightPadding)
75 | assertThat(plainOldView.height).isEqualTo(50 - topPadding - bottomPadding)
76 |
77 | assertThat(centeredView.left).isEqualTo(200 / 2 - 5)
78 | assertThat(centeredView.top).isEqualTo(50 / 2 - 5)
79 | assertThat(centeredView.right).isEqualTo(200 / 2 + 5)
80 | assertThat(centeredView.bottom).isEqualTo(50 / 2 + 5)
81 | assertThat(centeredView.width).isEqualTo(10)
82 | assertThat(centeredView.height).isEqualTo(10)
83 | }
84 |
85 | @Test
86 | fun `simple layout, with padding, respectsPadding disable`() {
87 | val plainOldView = View(activity)
88 | val centeredView = View(activity)
89 |
90 | val leftPadding = 1
91 | val topPadding = 2
92 | val rightPadding = 4
93 | val bottomPadding = 8
94 |
95 | val layout = contourLayout(
96 | context = activity,
97 | width = 200,
98 | height = 50
99 | ) {
100 | respectPadding = false
101 | setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
102 | plainOldView.layoutBy(
103 | leftTo { parent.left() }.rightTo { parent.right() },
104 | topTo { parent.top() }.bottomTo { parent.bottom() }
105 | )
106 | centeredView.layoutBy(
107 | centerHorizontallyTo { parent.centerX() }.widthOf { 10.xdip },
108 | centerVerticallyTo { parent.centerY() }.heightOf { 10.ydip }
109 | )
110 | }
111 |
112 | assertThat(plainOldView.left).isEqualTo(0)
113 | assertThat(plainOldView.top).isEqualTo(0)
114 | assertThat(plainOldView.right).isEqualTo(200)
115 | assertThat(plainOldView.bottom).isEqualTo(50)
116 | assertThat(plainOldView.width).isEqualTo(200)
117 | assertThat(plainOldView.height).isEqualTo(50)
118 |
119 | assertThat(centeredView.left).isEqualTo(200 / 2 - 5)
120 | assertThat(centeredView.top).isEqualTo(50 / 2 - 5)
121 | assertThat(centeredView.right).isEqualTo(200 / 2 + 5)
122 | assertThat(centeredView.bottom).isEqualTo(50 / 2 + 5)
123 | assertThat(centeredView.width).isEqualTo(10)
124 | assertThat(centeredView.height).isEqualTo(10)
125 |
126 | // Now re-enable respectPadding
127 | layout.respectPadding = true
128 | layout.forceRelayout()
129 |
130 | assertThat(plainOldView.left).isEqualTo(leftPadding)
131 | assertThat(plainOldView.top).isEqualTo(topPadding)
132 | assertThat(plainOldView.right).isEqualTo(200 - rightPadding)
133 | assertThat(plainOldView.bottom).isEqualTo(50 - bottomPadding)
134 | assertThat(plainOldView.width).isEqualTo(200 - leftPadding - rightPadding)
135 | assertThat(plainOldView.height).isEqualTo(50 - topPadding - bottomPadding)
136 |
137 | assertThat(centeredView.left).isEqualTo(200 / 2 - 5)
138 | assertThat(centeredView.top).isEqualTo(50 / 2 - 5)
139 | assertThat(centeredView.right).isEqualTo(200 / 2 + 5)
140 | assertThat(centeredView.bottom).isEqualTo(50 / 2 + 5)
141 | assertThat(centeredView.width).isEqualTo(10)
142 | assertThat(centeredView.height).isEqualTo(10)
143 | }
144 |
145 | @Test
146 | fun `child can be aligned to another child`() {
147 | val fakeImageView = View(activity)
148 | val fakeTextView = View(activity)
149 |
150 | contourLayout(
151 | context = activity,
152 | width = 200,
153 | height = 50
154 | ) {
155 | fakeImageView.layoutBy(
156 | leftTo { parent.left() }.widthOf {
157 | parent.height()
158 | .toX()
159 | },
160 | topTo { parent.top() }.heightOf { parent.height() }
161 | )
162 | fakeTextView.layoutBy(
163 | leftTo { fakeImageView.right() }.rightTo { parent.right() },
164 | topTo { parent.top() }.heightOf { parent.height() }
165 | )
166 | }
167 |
168 | assertThat(fakeImageView.width).isEqualTo(50)
169 | assertThat(fakeImageView.height).isEqualTo(50)
170 |
171 | assertThat(fakeTextView.top).isEqualTo(0)
172 | assertThat(fakeTextView.left).isEqualTo(50)
173 | assertThat(fakeTextView.width).isEqualTo(150)
174 | assertThat(fakeTextView.height).isEqualTo(50)
175 | }
176 |
177 | @Test
178 | fun `unspecified size will fallback to views preferred size`() {
179 | val fakeTextView = FakeTextView(activity, "Test", 10, 0)
180 |
181 | contourLayout(activity) {
182 | fakeTextView.layoutBy(
183 | leftTo { parent.left() },
184 | topTo { parent.top() }
185 | )
186 | }
187 |
188 | assertThat(fakeTextView.width).isEqualTo(40)
189 | assertThat(fakeTextView.height).isEqualTo(10)
190 | }
191 |
192 | @Test
193 | fun `specified exact size should be the final size`() {
194 | val view = View(activity)
195 | val fakeTextView = FakeTextView(activity, "Test", 10, 0)
196 |
197 | contourLayout(activity) {
198 | view.layoutBy(
199 | leftTo { parent.left() }.widthOf { 30.xdip },
200 | topTo { parent.top() }.heightOf { 15.ydip }
201 | )
202 | fakeTextView.layoutBy(
203 | leftTo { parent.left() }.widthOf { 50.xdip },
204 | topTo { parent.top() }.heightOf { 20.ydip }
205 | )
206 | }
207 |
208 | assertThat(view.width).isEqualTo(30)
209 | assertThat(view.height).isEqualTo(15)
210 | assertThat(fakeTextView.width).isEqualTo(50)
211 | assertThat(fakeTextView.height).isEqualTo(20)
212 | }
213 |
214 | @Test
215 | fun `minOf maxOf on x axis`() {
216 | val view0 = View(activity)
217 | val view1 = View(activity)
218 |
219 | var x0 = 10.toXInt()
220 | val x1 = 20.toXInt()
221 |
222 | val layout = contourLayout(activity) {
223 | view0.layoutBy(
224 | maxOf(leftTo { x0 }, leftTo { x1 }),
225 | topTo { parent.top() }
226 | )
227 | view1.layoutBy(
228 | minOf(leftTo { x0 }, leftTo { x1 }),
229 | topTo { parent.top() }
230 | )
231 | }
232 |
233 | assertThat(view0.left).isEqualTo(20)
234 | assertThat(view1.left).isEqualTo(10)
235 |
236 | x0 = 30.toXInt()
237 | layout.forceRelayout()
238 |
239 | assertThat(view0.left).isEqualTo(30)
240 | assertThat(view1.left).isEqualTo(20)
241 | }
242 |
243 | @Test
244 | fun `minOf maxOf on y axis`() {
245 | val view0 = View(activity)
246 | val view1 = View(activity)
247 |
248 | var y0 = 10.toYInt()
249 | val y1 = 20.toYInt()
250 |
251 | val layout = contourLayout(activity) {
252 | view0.layoutBy(
253 | leftTo { parent.left() },
254 | maxOf(topTo { y0 }, topTo { y1 })
255 | )
256 | view1.layoutBy(
257 | leftTo { parent.left() },
258 | minOf(topTo { y0 }, topTo { y1 })
259 | )
260 | }
261 |
262 | assertThat(view0.top).isEqualTo(20)
263 | assertThat(view1.top).isEqualTo(10)
264 |
265 | y0 = 30.toYInt()
266 | layout.forceRelayout()
267 |
268 | assertThat(view0.top).isEqualTo(30)
269 | assertThat(view1.top).isEqualTo(20)
270 | }
271 |
272 | @Test
273 | fun `width as fraction of parents width`() {
274 | val view = View(activity)
275 |
276 | var amount = 0.4f
277 |
278 | val layout = contourLayout(
279 | activity,
280 | width = 50
281 | ) {
282 | view.layoutBy(
283 | leftTo { parent.left() }.widthOfFloat { parent.width() * amount },
284 | centerVerticallyTo { parent.centerY() }
285 | )
286 | }
287 |
288 | assertThat(view.width).isEqualTo(20) // 50 * 0.4
289 |
290 | amount = 0.51f
291 | layout.forceRelayout()
292 | assertThat(view.width).isEqualTo(25) // 50 * 0.51 = 25.5 ~= 25 floored
293 | }
294 |
295 | @Test
296 | fun `height as fraction of parents height`() {
297 | val view = View(activity)
298 |
299 | var amount = 0.1f
300 |
301 | val layout = contourLayout(
302 | activity,
303 | width = 260
304 | ) {
305 | view.layoutBy(
306 | leftTo { parent.left() }.widthOfFloat { parent.width() * amount },
307 | centerVerticallyTo { parent.centerY() }
308 | )
309 | }
310 |
311 | assertThat(view.width).isEqualTo(26) // 260 * 0.1
312 |
313 | amount = 0.13f
314 | layout.forceRelayout()
315 | assertThat(view.width).isEqualTo(33) // 260 * 0.13 = 33.8 ~= 33 floored
316 | }
317 |
318 | @Test
319 | fun `conversion of XFloat to XInt and YFloat to YInt`() {
320 | val view = View(activity)
321 | contourLayout(activity, width = 260, height = 260) {
322 | view.layoutBy(
323 | leftTo { parent.left() }.widthOf { (parent.width() * 0.42f).toInt() },
324 | topTo { parent.top() }.heightOf { (parent.height() * 0.99f).toInt() }
325 | )
326 | }
327 | assertThat(view.width).isEqualTo(109) // 260 * 0.42 = 109.2 ~= 109 floored
328 | assertThat(view.height).isEqualTo(257) // 260 * 0.99 = 257.4 ~= 257 floored
329 | }
330 |
331 | @Test
332 | fun `conversion of XInt to XFloat and YInt to YFloat`() {
333 | val view = View(activity)
334 | contourLayout(activity, width = 260, height = 260) {
335 | view.layoutBy(
336 | leftTo { parent.left() }.widthOfFloat { parent.width().toFloat() },
337 | topTo { parent.top() }.heightOfFloat { parent.height().toFloat() }
338 | )
339 | }
340 | assertThat(view.width).isEqualTo(260)
341 | assertThat(view.height).isEqualTo(260)
342 | }
343 |
344 | @Test
345 | fun `view set to GONE does not get laid out and is considered to have position and size 0`() {
346 | val view = View(activity)
347 | view.visibility = View.GONE
348 |
349 | val layout = contourLayout(activity) {
350 | view.layoutBy(
351 | leftTo { parent.centerX() },
352 | topTo { parent.centerY() }
353 | )
354 | }
355 |
356 | assertThat(view.left).isEqualTo(0)
357 | assertThat(view.right).isEqualTo(0)
358 | assertThat(view.top).isEqualTo(0)
359 | assertThat(view.bottom).isEqualTo(0)
360 | assertThat(view.width).isEqualTo(0)
361 | assertThat(view.height).isEqualTo(0)
362 |
363 | // Now make it visible.
364 | view.visibility = View.VISIBLE
365 | layout.forceRelayout()
366 |
367 | assertThat(view.left).isEqualTo(100)
368 | assertThat(view.right).isEqualTo(100)
369 | assertThat(view.top).isEqualTo(25)
370 | assertThat(view.bottom).isEqualTo(25)
371 | assertThat(view.width).isEqualTo(0)
372 | assertThat(view.height).isEqualTo(0)
373 | }
374 |
375 | @Test
376 | fun `reference to height and width of view set to GONE should evaluate to 0`() {
377 | val viewThatIsGone = View(activity).apply { visibility = View.GONE }
378 | val otherView = View(activity)
379 |
380 | val layout = contourLayout(activity, width = 200, height = 50) {
381 | viewThatIsGone.layoutBy(
382 | leftTo { parent.centerX() }
383 | .widthOf { 10.xdip },
384 | topTo { parent.centerY() }
385 | .heightOf { 15.ydip }
386 | )
387 |
388 | otherView.layoutBy(
389 | leftTo { parent.left() + 5 }
390 | .widthOf { viewThatIsGone.width() + 1 },
391 | topTo { parent.top() + 10 }
392 | .heightOf { viewThatIsGone.height() + 2 }
393 | )
394 | }
395 |
396 | assertThat(otherView.left).isEqualTo(5)
397 | assertThat(otherView.right).isEqualTo(5 + 1)
398 | assertThat(otherView.top).isEqualTo(10)
399 | assertThat(otherView.bottom).isEqualTo(10 + 2)
400 | assertThat(otherView.width).isEqualTo(1)
401 | assertThat(otherView.height).isEqualTo(2)
402 |
403 | // Now make the view that was GONE visible.
404 | viewThatIsGone.visibility = View.VISIBLE
405 | layout.forceRelayout()
406 |
407 | assertThat(otherView.left).isEqualTo(5)
408 | assertThat(otherView.right).isEqualTo(5 + 10 + 1)
409 | assertThat(otherView.top).isEqualTo(10)
410 | assertThat(otherView.bottom).isEqualTo(10 + 15 + 2)
411 | assertThat(otherView.width).isEqualTo(10 + 1)
412 | assertThat(otherView.height).isEqualTo(15 + 2)
413 | }
414 |
415 | @Test
416 | fun `using other axis width constraint`() {
417 | val view = View(activity)
418 |
419 | contourLayout(context = activity, width = 200, height = 200) {
420 | view.layoutBy(
421 | leftTo { parent.left() }.rightTo { parent.right() - 50.dip },
422 | topTo { parent.top() }.heightOf { view.width().toY() }
423 | )
424 | }
425 |
426 | assertThat(view.left).isEqualTo(0)
427 | assertThat(view.top).isEqualTo(0)
428 | assertThat(view.right).isEqualTo(150)
429 | assertThat(view.bottom).isEqualTo(150)
430 | assertThat(view.width).isEqualTo(150)
431 | assertThat(view.height).isEqualTo(150)
432 | }
433 |
434 | @Test
435 | fun `using other axis height constraint`() {
436 | val view = View(activity)
437 |
438 | contourLayout(context = activity, width = 200, height = 200) {
439 | view.layoutBy(
440 | leftTo { parent.left() }.rightTo { view.height().toX() },
441 | topTo { parent.top() }.bottomTo { 125.ydip }
442 | )
443 | }
444 |
445 | assertThat(view.left).isEqualTo(0)
446 | assertThat(view.top).isEqualTo(0)
447 | assertThat(view.right).isEqualTo(125)
448 | assertThat(view.bottom).isEqualTo(125)
449 | assertThat(view.width).isEqualTo(125)
450 | assertThat(view.height).isEqualTo(125)
451 | }
452 |
453 | @Test
454 | fun `simple baseline`() {
455 | val fakeTextView = FakeTextView(activity, "Test", 10, 8)
456 |
457 | contourLayout(activity) {
458 | fakeTextView.layoutBy(
459 | leftTo { parent.left() },
460 | baselineTo { 20.ydip }
461 | )
462 | }
463 |
464 | assertThat(fakeTextView.top).isEqualTo(12)
465 | assertThat(fakeTextView.bottom).isEqualTo(22)
466 | assertThat(fakeTextView.height).isEqualTo(10)
467 | }
468 |
469 | @Test
470 | fun `baseline to baseline`() {
471 | val fakeTextView1 = FakeTextView(activity, "Hello", 18, 15)
472 | val fakeTextView2 = FakeTextView(activity, "World", 12, 10)
473 |
474 | contourLayout(activity) {
475 | fakeTextView1.layoutBy(
476 | leftTo { parent.left() },
477 | topTo { parent.top() }
478 | )
479 | fakeTextView2.layoutBy(
480 | rightTo { parent.right() },
481 | baselineTo { fakeTextView1.baseline() }
482 | )
483 | }
484 |
485 | assertThat(fakeTextView2.top).isEqualTo(5)
486 | assertThat(fakeTextView2.bottom).isEqualTo(17)
487 | assertThat(fakeTextView2.height).isEqualTo(12)
488 | }
489 |
490 | @Test
491 | fun `calling baseline() on an unmeasured view should correctly resolve its baseline`() {
492 | val view = View(activity)
493 | val text = FakeTextView(activity, "Test", 10, 8)
494 |
495 | contourLayout(activity) {
496 | view.layoutBy(
497 | leftTo { 0.xdip },
498 | topTo { text.baseline() }
499 | )
500 | text.layoutBy(
501 | leftTo { 0.xdip },
502 | topTo { 0.ydip }.bottomTo { 10.ydip }
503 | )
504 | }
505 |
506 | assertThat(view.top).isEqualTo(8)
507 | }
508 | }
509 |
--------------------------------------------------------------------------------
/contour/src/test/kotlin/com/squareup/contour/XFloatTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Square Inc.
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 com.squareup.contour
18 |
19 | import com.google.common.truth.Truth.assertThat
20 | import org.junit.Test
21 |
22 | class XFloatTest {
23 |
24 | @Test
25 | fun `plus operator`() {
26 | assertThat(XFloat(10f) + 10).isEqualTo(XFloat(20f))
27 | assertThat(XFloat(10f) + XInt(10)).isEqualTo(XFloat(20f))
28 | assertThat(XFloat(10f) + 10f).isEqualTo(XFloat(20f))
29 | assertThat(XFloat(10f) + XFloat(10f)).isEqualTo(XFloat(20f))
30 | }
31 |
32 | @Test
33 | fun `minus operator`() {
34 | assertThat(XFloat(20f) - 10).isEqualTo(XFloat(10f))
35 | assertThat(XFloat(20f) - XInt(10)).isEqualTo(XFloat(10f))
36 | assertThat(XFloat(20f) - 10f).isEqualTo(XFloat(10f))
37 | assertThat(XFloat(20f) - XFloat(10f)).isEqualTo(XFloat(10f))
38 | }
39 |
40 | @Test
41 | fun `times operator`() {
42 | assertThat(XFloat(10f) * 10).isEqualTo(XFloat(100f))
43 | assertThat(XFloat(10f) * XInt(10)).isEqualTo(XFloat(100f))
44 | assertThat(XFloat(10f) * 10f).isEqualTo(XFloat(100f))
45 | assertThat(XFloat(10f) * XFloat(10f)).isEqualTo(XFloat(100f))
46 | }
47 |
48 | @Test
49 | fun `div operator`() {
50 | assertThat(XFloat(10f) / 10).isEqualTo(XFloat(1f))
51 | assertThat(XFloat(10f) / XInt(10)).isEqualTo(XFloat(1f))
52 | assertThat(XFloat(10f) / 10f).isEqualTo(XFloat(1f))
53 | assertThat(XFloat(10f) / XFloat(10f)).isEqualTo(XFloat(1f))
54 | }
55 |
56 | @Test
57 | fun `compareTo operator`() {
58 | assertThat(XFloat(10f).compareTo(10)).isEqualTo(0)
59 | assertThat(XFloat(20f).compareTo(10)).isEqualTo(1)
60 | assertThat(XFloat(10f).compareTo(20)).isEqualTo(-1)
61 |
62 | assertThat(XFloat(10f).compareTo(XInt(10))).isEqualTo(0)
63 | assertThat(XFloat(20f).compareTo(XInt(10))).isEqualTo(1)
64 | assertThat(XFloat(10f).compareTo(XInt(20))).isEqualTo(-1)
65 |
66 | assertThat(XFloat(10f).compareTo(10f)).isEqualTo(0)
67 | assertThat(XFloat(20f).compareTo(10f)).isEqualTo(1)
68 | assertThat(XFloat(10f).compareTo(20f)).isEqualTo(-1)
69 |
70 | assertThat(XFloat(10f).compareTo(XFloat(10f))).isEqualTo(0)
71 | assertThat(XFloat(20f).compareTo(XFloat(10f))).isEqualTo(1)
72 | assertThat(XFloat(10f).compareTo(XFloat(20f))).isEqualTo(-1)
73 | }
74 |
75 | @Test
76 | fun `toY method`() {
77 | assertThat(XFloat(10f).toY()).isEqualTo(YFloat(10f))
78 | }
79 |
80 | @Test
81 | fun `toInt method`() {
82 | assertThat(XFloat(10f).toInt()).isEqualTo(XInt(10))
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/contour/src/test/kotlin/com/squareup/contour/XIntTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Square Inc.
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 com.squareup.contour
18 |
19 | import com.google.common.truth.Truth.assertThat
20 | import org.junit.Test
21 |
22 | class XIntTest {
23 |
24 | @Test
25 | fun `plus operator`() {
26 | assertThat(XInt(10) + 10).isEqualTo(XInt(20))
27 | assertThat(XInt(10) + XInt(10)).isEqualTo(XInt(20))
28 | assertThat(XInt(10) + 10f).isEqualTo(XFloat(20f))
29 | assertThat(XInt(10) + XFloat(10f)).isEqualTo(XFloat(20f))
30 | }
31 |
32 | @Test
33 | fun `minus operator`() {
34 | assertThat(XInt(20) - 10).isEqualTo(XInt(10))
35 | assertThat(XInt(20) - XInt(10)).isEqualTo(XInt(10))
36 | assertThat(XInt(20) - 10f).isEqualTo(XFloat(10f))
37 | assertThat(XInt(20) - XFloat(10f)).isEqualTo(XFloat(10f))
38 | }
39 |
40 | @Test
41 | fun `times operator`() {
42 | assertThat(XInt(10) * 10).isEqualTo(XInt(100))
43 | assertThat(XInt(10) * XInt(10)).isEqualTo(XInt(100))
44 | assertThat(XInt(10) * 10f).isEqualTo(XFloat(100f))
45 | assertThat(XInt(10) * XFloat(10f)).isEqualTo(XFloat(100f))
46 | }
47 |
48 | @Test
49 | fun `div operator`() {
50 | assertThat(XInt(10) / 10).isEqualTo(XInt(1))
51 | assertThat(XInt(10) / XInt(10)).isEqualTo(XInt(1))
52 | assertThat(XInt(10) / 10f).isEqualTo(XFloat(1f))
53 | assertThat(XInt(10) / XFloat(10f)).isEqualTo(XFloat(1f))
54 | }
55 |
56 | @Test
57 | fun `compareTo operator`() {
58 | assertThat(XInt(10).compareTo(10)).isEqualTo(0)
59 | assertThat(XInt(20).compareTo(10)).isEqualTo(1)
60 | assertThat(XInt(10).compareTo(20)).isEqualTo(-1)
61 |
62 | assertThat(XInt(10).compareTo(XInt(10))).isEqualTo(0)
63 | assertThat(XInt(20).compareTo(XInt(10))).isEqualTo(1)
64 | assertThat(XInt(10).compareTo(XInt(20))).isEqualTo(-1)
65 |
66 | assertThat(XInt(10).compareTo(10f)).isEqualTo(0)
67 | assertThat(XInt(20).compareTo(10f)).isEqualTo(1)
68 | assertThat(XInt(10).compareTo(20f)).isEqualTo(-1)
69 |
70 | assertThat(XInt(10).compareTo(XFloat(10f))).isEqualTo(0)
71 | assertThat(XInt(20).compareTo(XFloat(10f))).isEqualTo(1)
72 | assertThat(XInt(10).compareTo(XFloat(20f))).isEqualTo(-1)
73 | }
74 |
75 | @Test
76 | fun `toY method`() {
77 | assertThat(XInt(10).toY()).isEqualTo(YInt(10))
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/contour/src/test/kotlin/com/squareup/contour/YFloatTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Square Inc.
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 com.squareup.contour
18 |
19 | import com.google.common.truth.Truth.assertThat
20 | import org.junit.Test
21 |
22 | class YFloatTest {
23 |
24 | @Test
25 | fun `plus operator`() {
26 | assertThat(YFloat(10f) + 10).isEqualTo(YFloat(20f))
27 | assertThat(YFloat(10f) + YInt(10)).isEqualTo(YFloat(20f))
28 | assertThat(YFloat(10f) + 10f).isEqualTo(YFloat(20f))
29 | assertThat(YFloat(10f) + YFloat(10f)).isEqualTo(YFloat(20f))
30 | }
31 |
32 | @Test
33 | fun `minus operator`() {
34 | assertThat(YFloat(20f) - 10).isEqualTo(YFloat(10f))
35 | assertThat(YFloat(20f) - YInt(10)).isEqualTo(YFloat(10f))
36 | assertThat(YFloat(20f) - 10f).isEqualTo(YFloat(10f))
37 | assertThat(YFloat(20f) - YFloat(10f)).isEqualTo(YFloat(10f))
38 | }
39 |
40 | @Test
41 | fun `times operator`() {
42 | assertThat(YFloat(10f) * 10).isEqualTo(YFloat(100f))
43 | assertThat(YFloat(10f) * YInt(10)).isEqualTo(YFloat(100f))
44 | assertThat(YFloat(10f) * 10f).isEqualTo(YFloat(100f))
45 | assertThat(YFloat(10f) * YFloat(10f)).isEqualTo(YFloat(100f))
46 | }
47 |
48 | @Test
49 | fun `div operator`() {
50 | assertThat(YFloat(10f) / 10).isEqualTo(YFloat(1f))
51 | assertThat(YFloat(10f) / YInt(10)).isEqualTo(YFloat(1f))
52 | assertThat(YFloat(10f) / 10f).isEqualTo(YFloat(1f))
53 | assertThat(YFloat(10f) / YFloat(10f)).isEqualTo(YFloat(1f))
54 | }
55 |
56 | @Test
57 | fun `compareTo operator`() {
58 | assertThat(YFloat(10f).compareTo(10)).isEqualTo(0)
59 | assertThat(YFloat(20f).compareTo(10)).isEqualTo(1)
60 | assertThat(YFloat(10f).compareTo(20)).isEqualTo(-1)
61 |
62 | assertThat(YFloat(10f).compareTo(YInt(10))).isEqualTo(0)
63 | assertThat(YFloat(20f).compareTo(YInt(10))).isEqualTo(1)
64 | assertThat(YFloat(10f).compareTo(YInt(20))).isEqualTo(-1)
65 |
66 | assertThat(YFloat(10f).compareTo(10f)).isEqualTo(0)
67 | assertThat(YFloat(20f).compareTo(10f)).isEqualTo(1)
68 | assertThat(YFloat(10f).compareTo(20f)).isEqualTo(-1)
69 |
70 | assertThat(YFloat(10f).compareTo(YFloat(10f))).isEqualTo(0)
71 | assertThat(YFloat(20f).compareTo(YFloat(10f))).isEqualTo(1)
72 | assertThat(YFloat(10f).compareTo(YFloat(20f))).isEqualTo(-1)
73 | }
74 |
75 | @Test
76 | fun `toY method`() {
77 | assertThat(YFloat(10f).toX()).isEqualTo(XFloat(10f))
78 | }
79 |
80 | @Test
81 | fun `toInt method`() {
82 | assertThat(YFloat(10f).toInt()).isEqualTo(YInt(10))
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/contour/src/test/kotlin/com/squareup/contour/YIntTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Square Inc.
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 com.squareup.contour
18 |
19 | import com.google.common.truth.Truth.assertThat
20 | import org.junit.Test
21 |
22 | class YIntTest {
23 |
24 | @Test
25 | fun `plus operator`() {
26 | assertThat(YInt(10) + 10).isEqualTo(YInt(20))
27 | assertThat(YInt(10) + YInt(10)).isEqualTo(YInt(20))
28 | assertThat(YInt(10) + 10f).isEqualTo(YFloat(20f))
29 | assertThat(YInt(10) + YFloat(10f)).isEqualTo(YFloat(20f))
30 | }
31 |
32 | @Test
33 | fun `minus operator`() {
34 | assertThat(YInt(20) - 10).isEqualTo(YInt(10))
35 | assertThat(YInt(20) - YInt(10)).isEqualTo(YInt(10))
36 | assertThat(YInt(20) - 10f).isEqualTo(YFloat(10f))
37 | assertThat(YInt(20) - YFloat(10f)).isEqualTo(YFloat(10f))
38 | }
39 |
40 | @Test
41 | fun `times operator`() {
42 | assertThat(YInt(10) * 10).isEqualTo(YInt(100))
43 | assertThat(YInt(10) * YInt(10)).isEqualTo(YInt(100))
44 | assertThat(YInt(10) * 10f).isEqualTo(YFloat(100f))
45 | assertThat(YInt(10) * YFloat(10f)).isEqualTo(YFloat(100f))
46 | }
47 |
48 | @Test
49 | fun `div operator`() {
50 | assertThat(YInt(10) / 10).isEqualTo(YInt(1))
51 | assertThat(YInt(10) / YInt(10)).isEqualTo(YInt(1))
52 | assertThat(YInt(10) / 10f).isEqualTo(YFloat(1f))
53 | assertThat(YInt(10) / YFloat(10f)).isEqualTo(YFloat(1f))
54 | }
55 |
56 | @Test
57 | fun `compareTo operator`() {
58 | assertThat(YInt(10).compareTo(10)).isEqualTo(0)
59 | assertThat(YInt(20).compareTo(10)).isEqualTo(1)
60 | assertThat(YInt(10).compareTo(20)).isEqualTo(-1)
61 |
62 | assertThat(YInt(10).compareTo(YInt(10))).isEqualTo(0)
63 | assertThat(YInt(20).compareTo(YInt(10))).isEqualTo(1)
64 | assertThat(YInt(10).compareTo(YInt(20))).isEqualTo(-1)
65 |
66 | assertThat(YInt(10).compareTo(10f)).isEqualTo(0)
67 | assertThat(YInt(20).compareTo(10f)).isEqualTo(1)
68 | assertThat(YInt(10).compareTo(20f)).isEqualTo(-1)
69 |
70 | assertThat(YInt(10).compareTo(YFloat(10f))).isEqualTo(0)
71 | assertThat(YInt(20).compareTo(YFloat(10f))).isEqualTo(1)
72 | assertThat(YInt(10).compareTo(YFloat(20f))).isEqualTo(-1)
73 | }
74 |
75 | @Test
76 | fun `toX method`() {
77 | assertThat(YInt(10).toX()).isEqualTo(XInt(10))
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/contour/src/test/kotlin/com/squareup/contour/utils/ContourTestHelpers.kt:
--------------------------------------------------------------------------------
1 | package com.squareup.contour.utils
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import com.squareup.contour.ContourLayout
7 |
8 | fun T.layoutSizeOf(width: Int, height: Int): T {
9 | measure(
10 | View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
11 | View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
12 | )
13 | layout(0, 0, measuredWidth, measuredHeight)
14 | return this
15 | }
16 |
17 | fun contourLayout(
18 | context: Context,
19 | width: Int = 200,
20 | height: Int = 50,
21 | initializeLayout: ContourLayout.() -> Unit
22 | ): ContourLayout =
23 | object : ContourLayout(context) {
24 | init {
25 | initializeLayout()
26 | }
27 | }.layoutSizeOf(width, height)
28 |
29 | fun ViewGroup.forceRelayout() {
30 | requestLayout()
31 | layoutSizeOf(measuredWidth, measuredHeight)
32 | }
33 |
--------------------------------------------------------------------------------
/contour/src/test/kotlin/com/squareup/contour/utils/FakeTextView.kt:
--------------------------------------------------------------------------------
1 | package com.squareup.contour.utils
2 |
3 | import android.content.Context
4 | import android.view.View
5 |
6 | class FakeTextView(
7 | context: Context,
8 | private val text: String,
9 | private val textSize: Int,
10 | private val baseline: Int
11 | ) : View(context) {
12 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
13 | val availableWidth = MeasureSpec.getSize(widthMeasureSpec)
14 | val widthMode = MeasureSpec.getMode(widthMeasureSpec)
15 |
16 | val availableHeight = MeasureSpec.getSize(heightMeasureSpec)
17 | val heightMode = MeasureSpec.getMode(heightMeasureSpec)
18 |
19 | setMeasuredDimension(
20 | if (widthMode == MeasureSpec.EXACTLY) availableWidth
21 | else text.length * textSize,
22 | if (heightMode == MeasureSpec.EXACTLY) availableHeight
23 | else textSize
24 | )
25 | }
26 |
27 | override fun getBaseline(): Int = baseline
28 | }
29 |
--------------------------------------------------------------------------------
/contour/src/test/kotlin/com/squareup/contour/wrappers/ParentGeometryTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Square Inc.
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 com.squareup.contour.wrappers
18 |
19 | import android.graphics.Rect
20 | import com.google.common.truth.Truth.assertThat
21 | import com.squareup.contour.XInt
22 | import com.squareup.contour.YInt
23 | import com.squareup.contour.constraints.SizeConfig
24 | import org.junit.Test
25 |
26 | class ParentGeometryTest {
27 |
28 | private val paddingConfig = {
29 | Rect().apply {
30 | top = 10
31 | right = 20
32 | bottom = 30
33 | left = 40
34 | }
35 | }
36 | private val parentGeometry = ParentGeometry(
37 | widthConfig = SizeConfig(available = 120, lambda = { it }),
38 | heightConfig = SizeConfig(available = 60, lambda = { it }),
39 | paddingConfig = paddingConfig
40 | )
41 |
42 | @Test
43 | fun `left method`() {
44 | assertThat(parentGeometry.left()).isEqualTo(XInt(40))
45 | }
46 |
47 | @Test
48 | fun `right method`() {
49 | assertThat(parentGeometry.right()).isEqualTo(XInt(100))
50 | }
51 |
52 | @Test
53 | fun `width method`() {
54 | assertThat(parentGeometry.width()).isEqualTo(XInt(120))
55 | }
56 |
57 | @Test
58 | fun `centerX method`() {
59 | assertThat(parentGeometry.centerX()).isEqualTo(XInt(60))
60 | }
61 |
62 | @Test
63 | fun `top method`() {
64 | assertThat(parentGeometry.top()).isEqualTo(YInt(10))
65 | }
66 |
67 | @Test
68 | fun `bottom method`() {
69 | assertThat(parentGeometry.bottom()).isEqualTo(YInt(30))
70 | }
71 |
72 | @Test
73 | fun `height method`() {
74 | assertThat(parentGeometry.height()).isEqualTo(YInt(60))
75 | }
76 |
77 | @Test
78 | fun `centerY method`() {
79 | assertThat(parentGeometry.centerY()).isEqualTo(YInt(30))
80 | }
81 |
82 | @Test
83 | fun `padding method`() {
84 | assertThat(parentGeometry.padding().top).isEqualTo(10)
85 | assertThat(parentGeometry.padding().right).isEqualTo(20)
86 | assertThat(parentGeometry.padding().bottom).isEqualTo(30)
87 | assertThat(parentGeometry.padding().left).isEqualTo(40)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | android.useAndroidX=true
2 | # Disable BuildConfig feature by default because no modules in this repository use it
3 | android.defaults.buildfeatures.buildconfig=false
4 | org.gradle.jvmargs=-Xmx2G
5 |
6 | VERSION_NAME=1.2.0-SNAPSHOT
7 | GROUP=app.cash.contour
8 |
9 | POM_DESCRIPTION=A programmatic layout library for Android
10 | POM_URL=https://github.com/cashapp/contour
11 | POM_SCM_URL=https://github.com/cashapp/contour
12 | POM_SCM_CONNECTION=scm:git:git://github.com/cashapp/contour.git
13 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/cashapp/contour.git
14 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
15 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
16 | POM_LICENCE_DIST=repo
17 | POM_DEVELOPER_ID=square
18 | POM_DEVELOPER_NAME=Square, Inc.
19 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/contour/221325561e1f5eb20b7cd7724d2fc8cad26a8ab1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MSYS* | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/sample-app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 |
4 | android {
5 | compileSdkVersion versions.targetSdk
6 |
7 | defaultConfig {
8 | applicationId "com.squareup.contour.sample"
9 | minSdkVersion versions.minSdk
10 | targetSdkVersion versions.targetSdk
11 | }
12 |
13 | compileOptions {
14 | sourceCompatibility JavaVersion.VERSION_1_8
15 | targetCompatibility JavaVersion.VERSION_1_8
16 | }
17 | }
18 |
19 | dependencies {
20 | implementation deps.androidx.appCompat
21 | implementation deps.picasso
22 | implementation deps.androidx.transition
23 | implementation deps.androidx.ktx
24 | implementation project(':contour')
25 | }
--------------------------------------------------------------------------------
/sample-app/gradle.properties:
--------------------------------------------------------------------------------
1 | #remove after nexr Picasso release > 2.e
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/sample-app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/sample-app/src/main/java/com/squareup/contour/sample/CircularImageView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.sample
18 |
19 | import android.content.Context
20 | import android.graphics.Canvas
21 | import android.graphics.Color
22 | import android.graphics.Paint
23 | import android.graphics.Path
24 | import androidx.appcompat.widget.AppCompatImageView
25 |
26 | class CircularImageView(context: Context) : AppCompatImageView(context) {
27 | private val path = Path()
28 | override fun onDraw(canvas: Canvas) {
29 | val r = width / 2f
30 | path.reset()
31 | path.addCircle(r, r, r, Path.Direction.CW)
32 | canvas.clipPath(path)
33 | super.onDraw(canvas)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/sample-app/src/main/java/com/squareup/contour/sample/Colors.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.sample
18 |
19 | const val Blue = 0xFF2A4F6E.toInt()
20 | const val Yellow = 0xFFAA8E39.toInt()
21 | const val Red = 0xFFAA5A39.toInt()
22 | const val Black = 0xFF000000.toInt()
23 | const val White = 0xFFFFFFFF.toInt()
24 |
--------------------------------------------------------------------------------
/sample-app/src/main/java/com/squareup/contour/sample/ExpandableBioCard1.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.sample
18 |
19 | import android.annotation.SuppressLint
20 | import android.content.Context
21 | import android.graphics.Color
22 | import android.graphics.Color.DKGRAY
23 | import android.graphics.Color.WHITE
24 | import android.graphics.drawable.PaintDrawable
25 | import android.text.TextUtils.TruncateAt.END
26 | import android.util.AttributeSet
27 | import android.view.ViewGroup
28 | import android.widget.ImageView.ScaleType.CENTER_CROP
29 | import android.widget.TextView
30 | import androidx.core.view.updatePadding
31 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator
32 | import androidx.transition.AutoTransition
33 | import androidx.transition.TransitionManager
34 | import com.squareup.contour.ContourLayout
35 | import com.squareup.picasso.Picasso
36 |
37 | @SuppressLint("SetTextI18n")
38 | class ExpandableBioCard1(context: Context, attrs: AttributeSet? = null) : ContourLayout(context, attrs) {
39 | private val avatar = CircularImageView(context).apply {
40 | scaleType = CENTER_CROP
41 |
42 | if (isInEditMode) {
43 | setBackgroundColor(Color.GRAY)
44 | } else {
45 | Picasso.get().load("https://upload.wikimedia.org/wikipedia/en/9/92/BenSisko.jpg").into(this)
46 | }
47 | }
48 |
49 | private val bio = TextView(context).apply {
50 | textSize = 14f
51 | text = "The Bajorans who have lived with us on this station, who have worked with us for months, " +
52 | "who helped us move this station to protect the wormhole, who joined us to explore the " +
53 | "Gamma Quadrant, who have begun to build the future of Bajor with us. These people know " +
54 | "that we are neither the enemy nor the devil. We don't always agree. We have some damn " +
55 | "good fights, in fact. But we always come away from them with a little better " +
56 | "understanding and appreciation of the other."
57 | ellipsize = END
58 | maxLines = 2
59 | setLineSpacing(0f, 1.33f)
60 | setTextColor(WHITE)
61 | }
62 |
63 | init {
64 | background = PaintDrawable(DKGRAY).also { it.setCornerRadius(32f) }
65 | clipToOutline = true
66 | elevation = 20f.dip
67 | stateListAnimator = PushOnPressAnimator(this)
68 | updatePadding(left = 16.dip, right = 16.dip)
69 |
70 | contourHeightOf { maxOf(avatar.bottom() + 24.ydip, bio.bottom() + 16.ydip) }
71 |
72 | avatar.layoutBy(
73 | x = leftTo { parent.left() }.widthOf { 60.xdip },
74 | y = topTo { parent.top() + 24.ydip }.heightOf { 60.ydip }
75 | )
76 | bio.layoutBy(
77 | x = leftTo { avatar.right() + 16.xdip }.rightTo { parent.right() },
78 | y = topTo {
79 | when {
80 | isSelected -> parent.top() + 16.ydip
81 | else -> avatar.centerY() - bio.preferredHeight() / 2
82 | }
83 | }
84 | )
85 |
86 | val collapsedLines = bio.maxLines
87 | setOnClickListener {
88 | TransitionManager.beginDelayedTransition(parent as ViewGroup, AutoTransition()
89 | .setInterpolator(FastOutSlowInInterpolator())
90 | .setDuration(300)
91 | )
92 |
93 | isSelected = !isSelected
94 | bio.maxLines = if (isSelected) Int.MAX_VALUE else collapsedLines
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/sample-app/src/main/java/com/squareup/contour/sample/ExpandableBioCard2.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.sample
18 |
19 | import android.animation.ObjectAnimator
20 | import android.annotation.SuppressLint
21 | import android.content.Context
22 | import android.graphics.Color
23 | import android.graphics.Color.DKGRAY
24 | import android.graphics.Typeface.BOLD
25 | import android.graphics.drawable.ColorDrawable
26 | import android.graphics.drawable.PaintDrawable
27 | import android.util.AttributeSet
28 | import android.view.KeyEvent
29 | import android.view.KeyEvent.KEYCODE_BACK
30 | import android.widget.ImageView
31 | import android.widget.ImageView.ScaleType.CENTER_CROP
32 | import android.widget.TextView
33 | import com.squareup.contour.ContourLayout
34 | import com.squareup.picasso.Picasso
35 |
36 | @SuppressLint("SetTextI18n")
37 | class ExpandableBioCard2(context: Context, attrs: AttributeSet? = null) : ContourLayout(context, attrs) {
38 | private val imageView = ImageView(context).apply {
39 | scaleType = CENTER_CROP
40 | setBackgroundColor(Color.GRAY)
41 |
42 | if (!isInEditMode) {
43 | Picasso.get()
44 | .load("https://i.imgur.com/ajdangY.jpg")
45 | .placeholder(ColorDrawable(Color.parseColor("#3d3d3d")))
46 | .into(this)
47 | }
48 | }
49 |
50 | private val title = TextView(context).apply {
51 | textSize = 16f
52 | text = "Nicolas Cage"
53 | setTextColor(White)
54 | setTypeface(typeface, BOLD)
55 | }
56 |
57 | private val bio = TextView(context).apply {
58 | textSize = 16f
59 | text = "Nicolas Kim Coppola, known professionally as Nicolas Cage, is an American actor" +
60 | "and filmmaker. Cage has been nominated for numerous major cinematic awards, and" +
61 | "won an Academy Award, a Golden Globe, and Screen Actors Guild Award for his performance" +
62 | "in Leaving Las Vegas."
63 | setLineSpacing(0f, 1.33f)
64 | }
65 |
66 | init {
67 | background = PaintDrawable(DKGRAY)
68 | clipToOutline = true
69 | elevation = 20f.dip
70 | stateListAnimator = PushOnPressAnimator(this)
71 | registerBackPressListener()
72 | toggleCornerRadius(show = true)
73 |
74 | contourHeightOf { available ->
75 | if (isSelected) available else title.bottom() + 20.ydip
76 | }
77 |
78 | bio.layoutBy(
79 | x = matchParentX(marginLeft = 20.dip, marginRight = 20.dip),
80 | y = topTo { title.bottom() + 20.ydip }
81 | )
82 | title.layoutBy(
83 | x = matchParentX(marginLeft = 20.dip, marginRight = 20.dip),
84 | y = topTo { imageView.bottom() + 20.ydip }
85 | )
86 | imageView.layoutBy(
87 | x = matchParentX(),
88 | y = topTo { parent.top() }.heightOf { 200.ydip }
89 | )
90 | }
91 |
92 | override fun getBackground() = super.getBackground() as PaintDrawable
93 |
94 | override fun setSelected(selected: Boolean) {
95 | if (isLaidOut && selected == this.isSelected) return
96 | super.setSelected(selected)
97 | toggleCornerRadius(show = !selected)
98 | }
99 |
100 | private fun toggleCornerRadius(show: Boolean) {
101 | // No idea why, but 0.0f causes the view to hide on animation end. Using 0.01 instead.
102 | val fromRadius = if (show) 0.01f else 12f.dip
103 | val toRadius = if (show) 12f.dip else 0.01f
104 |
105 | if (isLaidOut) {
106 | ObjectAnimator.ofFloat(fromRadius, toRadius)
107 | .apply { addUpdateListener { background.setCornerRadius(it.animatedValue as Float) } }
108 | .setDuration(200)
109 | .start()
110 | } else {
111 | background.setCornerRadius(toRadius)
112 | }
113 | }
114 |
115 | private fun registerBackPressListener() {
116 | isFocusableInTouchMode = true
117 | requestFocus()
118 | setOnKeyListener { _, keyCode, event ->
119 | if (isSelected && keyCode == KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
120 | performClick()
121 | } else {
122 | false
123 | }
124 | }
125 | }
126 | }
--------------------------------------------------------------------------------
/sample-app/src/main/java/com/squareup/contour/sample/PushOnPressAnimator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.sample
18 |
19 | import android.animation.Animator
20 | import android.animation.ObjectAnimator
21 | import android.animation.PropertyValuesHolder
22 | import android.animation.StateListAnimator
23 | import android.view.View
24 | import android.view.View.SCALE_X
25 | import android.view.View.SCALE_Y
26 | import android.view.animation.AccelerateDecelerateInterpolator
27 |
28 | /** Plays a subtle push animation when [view] is pressed. */
29 | class PushOnPressAnimator(private val view: View) : StateListAnimator() {
30 | init {
31 | addState(
32 | intArrayOf(android.R.attr.state_pressed),
33 | createAnimator(toScale = 0.95f)
34 | )
35 | addState(
36 | intArrayOf(-android.R.attr.state_pressed),
37 | createAnimator(toScale = 1f)
38 | )
39 | }
40 |
41 | private fun createAnimator(toScale: Float): Animator {
42 | val scaleX = PropertyValuesHolder.ofFloat(SCALE_X, toScale)
43 | val scaleY = PropertyValuesHolder.ofFloat(SCALE_Y, toScale)
44 | return ObjectAnimator.ofPropertyValuesHolder(view, scaleX, scaleY).apply {
45 | duration = 80
46 | interpolator = AccelerateDecelerateInterpolator()
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/sample-app/src/main/java/com/squareup/contour/sample/SampleActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.sample
18 |
19 | import android.os.Bundle
20 | import android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
21 | import android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
22 | import androidx.appcompat.app.AppCompatActivity
23 |
24 | class SampleActivity : AppCompatActivity() {
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | window.addFlags(FLAG_TRANSLUCENT_STATUS or FLAG_TRANSLUCENT_NAVIGATION)
27 | super.onCreate(savedInstanceState)
28 | setContentView(SampleView(this))
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/sample-app/src/main/java/com/squareup/contour/sample/SampleView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.sample
18 |
19 | import android.annotation.SuppressLint
20 | import android.content.Context
21 | import android.graphics.Color
22 | import android.graphics.Color.WHITE
23 | import android.graphics.Typeface
24 | import android.graphics.Typeface.NORMAL
25 | import android.util.AttributeSet
26 | import android.view.Gravity.CENTER_VERTICAL
27 | import android.view.WindowInsets
28 | import android.view.animation.OvershootInterpolator
29 | import android.widget.TextView
30 | import androidx.core.view.updatePadding
31 | import androidx.transition.ChangeBounds
32 | import androidx.transition.TransitionManager
33 | import com.squareup.contour.ContourLayout
34 |
35 | @SuppressLint("SetTextI18n")
36 | class SampleView(context: Context, attrs: AttributeSet? = null) : ContourLayout(context, attrs) {
37 | private val toolbar = TextView(context).apply {
38 | gravity = CENTER_VERTICAL
39 | letterSpacing = 0.05f
40 | text = "Contour"
41 | textSize = 18f
42 | typeface = Typeface.create("sans-serif-medium", NORMAL)
43 | setTextColor(WHITE)
44 | updatePadding(left = 26.dip)
45 | }
46 |
47 | private val card1 = ExpandableBioCard1(context)
48 | private val card2 = ExpandableBioCard2(context)
49 |
50 | init {
51 | setBackgroundColor(Color.parseColor("#303030"))
52 | contourHeightMatchParent()
53 |
54 | toolbar.layoutBy(
55 | x = leftTo { parent.left() },
56 | y = topTo { parent.top() + 4.ydip }.heightOf { 56.ydip }
57 | )
58 | card1.layoutBy(
59 | x = matchParentX(marginLeft = 24.dip, marginRight = 24.dip),
60 | y = topTo { toolbar.bottom() + 8.ydip }
61 | )
62 |
63 | val xPadding = { if (card2.isSelected) 0.xdip else 24.xdip }
64 | card2.layoutBy(
65 | x = leftTo { parent.left() + xPadding() }.rightTo { parent.right() - xPadding() },
66 | y = topTo {
67 | if (card2.isSelected) parent.top() else card1.bottom() + 24.ydip
68 | }.heightOf {
69 | if (card2.isSelected) parent.height() else card2.preferredHeight()
70 | }
71 | )
72 |
73 | card2.setOnClickListener {
74 | TransitionManager.beginDelayedTransition(this, ChangeBounds()
75 | .setInterpolator(OvershootInterpolator(1f))
76 | .setDuration(400)
77 | )
78 |
79 | card2.isSelected = !card2.isSelected
80 | requestLayout()
81 | }
82 | }
83 |
84 | override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
85 | updatePadding(top = insets.systemWindowInsetTop)
86 | return super.onApplyWindowInsets(insets)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/sample-app/src/main/java/com/squareup/contour/sample/widget/PaddingAdjusterWidget.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Square Inc.
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 com.squareup.contour.sample.widget
18 |
19 | import android.annotation.SuppressLint
20 | import android.graphics.Rect
21 | import android.widget.SeekBar
22 | import androidx.appcompat.widget.AppCompatSeekBar
23 | import androidx.appcompat.widget.AppCompatTextView
24 | import com.squareup.contour.ContourLayout
25 | import com.squareup.contour.sample.SampleActivity
26 |
27 | @SuppressLint("ViewConstructor")
28 | internal class PaddingAdjusterWidget(
29 | context: SampleActivity,
30 | private val onPaddingAdjusted: (Rect) -> Unit
31 | ): ContourLayout(context) {
32 |
33 | private val onChangeListener = object : SeekBar.OnSeekBarChangeListener {
34 | override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
35 | onPaddingAdjusted(Rect(
36 | leftSeek.progress,
37 | topSeek.progress,
38 | rightSeek.progress,
39 | bottomSeek.progress
40 | ))
41 | }
42 |
43 | override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
44 | override fun onStopTrackingTouch(seekBar: SeekBar?) = Unit
45 | }
46 |
47 | private val leftLabel = AppCompatTextView(context).apply {
48 | text = "Left"
49 | }
50 | private val topLabel = AppCompatTextView(context).apply {
51 | text = "Top"
52 | }
53 | private val rightLabel = AppCompatTextView(context).apply {
54 | text = "Right"
55 | }
56 | private val bottomLabel = AppCompatTextView(context).apply {
57 | text = "Bottom"
58 | }
59 |
60 | private val leftSeek: AppCompatSeekBar = AppCompatSeekBar(context).apply {
61 | setOnSeekBarChangeListener(onChangeListener)
62 | }
63 | private val topSeek: AppCompatSeekBar = AppCompatSeekBar(context).apply {
64 | setOnSeekBarChangeListener(onChangeListener)
65 | }
66 | private val rightSeek: AppCompatSeekBar = AppCompatSeekBar(context).apply {
67 | setOnSeekBarChangeListener(onChangeListener)
68 | }
69 | private val bottomSeek: AppCompatSeekBar = AppCompatSeekBar(context).apply {
70 | setOnSeekBarChangeListener(onChangeListener)
71 | }
72 |
73 | init {
74 | setPadding(
75 | 15.dip,
76 | 10.dip,
77 | 15.dip,
78 | 10.dip
79 | )
80 |
81 | leftLabel.layoutBy(
82 | x = leftTo { parent.left() },
83 | y = topTo { parent.top() }
84 | )
85 |
86 | topLabel.layoutBy(
87 | x = leftTo { parent.left() },
88 | y = topTo { leftLabel.bottom() + 10.dip }
89 | )
90 |
91 | rightLabel.layoutBy(
92 | x = leftTo { parent.left() },
93 | y = topTo { topLabel.bottom() + 10.dip }
94 | )
95 |
96 | bottomLabel.layoutBy(
97 | x = leftTo { parent.left() },
98 | y = topTo { rightLabel.bottom() + 10.dip }
99 | )
100 |
101 | leftSeek.layoutBy(
102 | x = leftTo { leftLabel.right() + 20.dip }
103 | .rightTo { parent.right() },
104 | y = centerVerticallyTo { leftLabel.centerY() }
105 | )
106 |
107 | topSeek.layoutBy(
108 | x = leftTo { topLabel.right() + 20.dip }
109 | .rightTo { parent.right() },
110 | y = centerVerticallyTo { topLabel.centerY() }
111 | )
112 |
113 | rightSeek.layoutBy(
114 | x = leftTo { rightLabel.right() + 20.dip }
115 | .rightTo { parent.right() },
116 | y = centerVerticallyTo { rightLabel.centerY() }
117 | )
118 |
119 | bottomSeek.layoutBy(
120 | x = leftTo { bottomLabel.right() + 20.dip }
121 | .rightTo { parent.right() },
122 | y = centerVerticallyTo { bottomLabel.centerY() }
123 | )
124 |
125 | contourHeightOf { maxOf(bottomSeek.bottom(), bottomLabel.bottom()) + paddingBottom }
126 | }
127 | }
--------------------------------------------------------------------------------
/sample-app/src/main/res/drawable/android_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/sample-app/src/main/res/drawable/check_mark.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/screenshots/crd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/contour/221325561e1f5eb20b7cd7724d2fc8cad26a8ab1/screenshots/crd.png
--------------------------------------------------------------------------------
/screenshots/droidcon_talk_cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/contour/221325561e1f5eb20b7cd7724d2fc8cad26a8ab1/screenshots/droidcon_talk_cover.png
--------------------------------------------------------------------------------
/screenshots/runtime_layout_logic.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/contour/221325561e1f5eb20b7cd7724d2fc8cad26a8ab1/screenshots/runtime_layout_logic.gif
--------------------------------------------------------------------------------
/screenshots/simple_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/contour/221325561e1f5eb20b7cd7724d2fc8cad26a8ab1/screenshots/simple_demo.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'contour-parent'
2 |
3 | include ':contour'
4 | include ':contour-lint'
5 | include ':sample-app'
6 |
--------------------------------------------------------------------------------