- Different usage of Item (including its siblings) in different contexts: directly in UI layout, in a ViewGroup, RecyclerView or ListView (via AlertDialog)
-
- AdaptUIShowcaseItem
-
+ Different usage of Item (including its siblings) in different contexts: directly in UI layout, in a ViewGroup, RecyclerView or ListView (via AlertDialog)
+
+ AdaptUIShowcaseItem
+
+
+
+
+
diff --git a/README.md b/README.md
index 1e004334..6169b129 100644
--- a/README.md
+++ b/README.md
@@ -1,52 +1,56 @@
-
+
-# Adapt & AdaptUI
-
-__Adapt__ is a UI library to create decoupled widget components. They can be
- used in a `RecyclerView`, `ListView`, inside a `LinearLayout` or used directly as a `View` interchangeably,
- no code involved. One `Item` to rule them all. Layout preview enabled.
-
-__AdaptUI__ is en enhanced Android view DSL builder that brings together dynamism
- and flexibility of __Adapt__ to native Android views. It aims to provide convenience
- and peace of mind for developers, meanwhile fixing pain points
- of Android XML - missing composability, reuse and customization. They are all included out of box.
- It is a _disappearing_
- view and layout builder that gives total control of created views, without imposing any
- limitations or forcing the use of certain tools or compilers. A view is a view. As it should have been.
+# Adapt & AdaptUI
## Install
-[](http://search.maven.org/#search|ga|1|g%3A%22io.noties%22%20AND%20a%3A%22adapt%22)
+[](http://search.maven.org/#search|ga|1|g%3A%22io.noties.adapt%22%20AND%20a%3A%22adapt%22)
```gradle
-implementation "io.noties:adapt:${adaptVersion}"
-implementation "io.noties:adapt-ui:${adaptVersion}"
-implementation "io.noties:adapt-ui-flex:${adaptVersion}"
+implementation platform('io.noties.adapt:bom:6.0')
+
+implementation 'io.noties.adapt:adapt'
+implementation 'io.noties.adapt:adapt-kt'
+implementation 'io.noties.adapt:adapt-ui'
+implementation 'io.noties.adapt:adapt-ui-flex'
```
## [AdaptUI](./adapt-ui/README.md)
-
-🚧 All showcase previews can be accessed via [dedicate page](./PREVIEW_SHOWCASE.md)
+> Fluent (no-xml) Android-View DSL in Kotlin.
+
+With flexibility in mind and total control over the process.
+Can be used as an enhancement over existing native Android widgets and layouts.
+Creates advanced Android views and layouts in openly-explorable and readable way.
+Influenced by SwiftUI. Adapted to Kotlin.
-🚧 Documentation might still be a bit lacking, but most of the features in `adapt-ui` module
- come with a [dedicated sample](./sample/src/main/java/io/noties/adapt/sample/samples) class file.
- What better can explain the functionality than the code, right? ;)
+
+🖼️ More previews like this
+➡️ Continue reading
+---
+
+🚧 \[Documentation is under construction]. Meanwhile, most of the features
+ come with a [dedicated sample or samples](./sample/src/main/java/io/noties/adapt/sample/samples). They include
+ always relevant code that could be also previewed in the [installed sample application](./releases/latest).
+ Along with Layout Preview in Android Studio to play-around.
+
+---
## [Adapt](./adapt/README.md)
-
-
+Android **true** adapter. ViewGroup _agnostique_ adapter that interchangeably
+renders items across `RecyclerView`, `ListView`, `ViewPager`, `LinearLayout`, `FlexboxLayout`, or any other `ViewGroup`.
-🚧 Documentation might still be a bit lacking, but most of the features in `adapt` module
- come with a [dedicated sample](./sample/src/main/java/io/noties/adapt/sample/samples) class file.
- What better can explain the functionality than the code, right? ;)
+
+
+
+[➡️ Continue reading](./adapt/README.md)
## License
```
- Copyright 2021 Dimitry Ivanov (legal@noties.io)
+ Copyright 2026 Dimitry Ivanov (hey@noties.io)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -59,4 +63,4 @@ implementation "io.noties:adapt-ui-flex:${adaptVersion}"
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-```
\ No newline at end of file
+```
diff --git a/README_TODO.md b/README_TODO.md
index 5d6c354f..cc0fbedc 100644
--- a/README_TODO.md
+++ b/README_TODO.md
@@ -1,5 +1,7 @@
# Adapt and AdaptUI
+YES! It *is* possible to have __native__ Android UI without XML!
+
## What it is?
AdaptUI came as a solution to reduce context switches between code and various XML files. Along with
@@ -13,33 +15,12 @@ components on Android. A single-file component reduces context switches, allows
modification of view building blocks meanwhile giving an immediate feedback with the help of the
preview. It keeps the loop running without the breaks.
-the problem is actually that SwiftUI is a complexity-hiding abstraction.
-No a complexity-hiding abstraction, if it does not work, go to the native view layer directly.
-Almost disappearing framework? Can be used to create view, create and update or just update?... no, this
-is not true, we cannot update without being created, well, in theory, we could use an Element and pass it a view, but
-this is crazy
Copying when needed a minor change, non extensible, no configurable, values are limited to be provided by xml,
make a padding `@dimen/content_padding + @dimen/additional_padding` is impossible leads to generating
more layers of indirection
Comment a line in XML - error, cannot do it
-Moreover, with AdaptUI does not make commitment.. It does not require a special compiler - all it is
-using is Kotlin code. All it operates on - native Android views. It does provide conveniences on top
-of native views, but does not restrict access. You still can access a view underneath, create own
-elements, or extensions on elements.
-
-AdaptUI has been inspired by Flutter, Combine and SwiftUI. Important difference from Flutter is the
-direction is which elements are build - in Flutter you wrap target views with customization views,
-like Padding, SizedBox, etc, so in the end initial view becomes wrapped under multiple layers of
-customization - like a cabbage. It hurts readability and discoverability. Views also expect to
-receive all its arguments in constructor, which leaves little Flutter is like a pockemon cabbage.
-Target (conceptually important)
-views are hidden by multiple pockemon layers with weird names and zero discoverability. In order to
-find what it is inside (what view)
-it holds you need to unwrap the layers, which make a lot of noise whilst you do that. Easier to copy
-code, see how it behaves, just comment related code lines, no need to modify structure. The same is
-re
Another things that {{positively}} distinguishes AdaptUI is AdaptUI does not come with any state
system, it can work with any.
diff --git a/TODO.md b/TODO.md
index 6b54f5c3..e8f9dd10 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,14 +1,39 @@
-* explicit tags in sample (enum?), define colors for each
+- [ ] adapt.items processor to add, filter, map, everything (add dividers, conditional, remove)
+-
+- [ ] view-pager class not found in a new project in preview
+- [ ] debug keep screen on
+- [ ] convert to bitmap
+- [ ] preview layouts (now only items and UI, but no view)
+- [ ] processor to generate strings, colors, dimensions
+- [ ] dialog (with window-insets handling)
+- [ ] update logo, make gradient and new asset
+- [ ] `AdaptUIScrollIfScrollsSample` transition for parent and layout fill (maybe missing), review all samples
+ that could be affected by new view layout (so maybe they need to be set to match-parent)
+- [ ] unify preview class names, `Preview_Sample`, `PreviewSample`, `Preview__Sample`
+- [ ] verify all tags are specified in `Tags` object
+- [ ] move `PreviewUtils` (adapt + adapt-ui) to `preview` package instead of `util`
+- [ ] add consumer proguard to remove preview layouts (ui and regular)
+- [ ].
+ > Adapt README Item is a chunk of view logic (along with actual view attached) that could be
+ > rendered and passed around.
+- [ ].
+ > Resource generator for colors, strings, drawables? (drawables? like icons)
+- [ ].
+ > Image loader (`AsyncImage`)
+- [ ] Shape.shadow Text/Label.textShadow ColorBuilder
+
+- [ ] VScroll and HScroll are actually expose ViewFactory, so
+ nested children might be able to add certain FrameLayout elements or layout customization,
+ meanwhile they are in different context.
+
+- [ ] `Text` autosize must be applied when text changes (maxLines?)
- [ ] SHOW, a layout with rounded background, icon and text => just a text with padding and shape
plus, clickable, foreground, cliptooutline
-- on view pred draw should have `once` as it delivers callback only once
-- [ ] shape, padding, for ex top, can result in rect.top be greater than bottom (we do not touch bottom)...
+- [ ] shape, padding, for ex top, can result in rect.top be greater than bottom (we do not touch
+ bottom)...
NOPE, convert to dp
-- [ ] maybe LP typealias? does ot solve anything, as we still need to specify generic variant, which would cause name collision
-- [ ] common interface for shape and stateful-shape
-- [ ] StatfulShape.create instead of `drawable`
- [ ] stateful-shape:
```
// TODO: maybe make more fluent, like
@@ -21,13 +46,35 @@
- [ ] maybe make viewElement open? but what would we achieve? most extensions use `ViewElement`,
so type information would not be preserved
-- [ ] add consumer proguard to remove preview layouts (ui and regular)
- [ ] review all property references that we have and reduce the amount? generates additional code
- [ ] investigate the size... inline onView? and most of the extsniosn?
- [ ] window insets
-- [ ] element+extensions, accessibility properties
- [ ] view, additional gestures
+---------------------------------------------------------------------------------------------------
+---------------------------------------------------------------------------------------------------
+---- DONE - DONE - DONE - DONE - DONE - DONE - DONE - DONE - DONE - DONE - DONE - DONE - DONE -----
+---------------------------------------------------------------------------------------------------
+---------------------------------------------------------------------------------------------------
+
+- [x] run all tests (sample.verifyPaDe) - added `test.sh`
+- [X] StatfulShape.create instead of `drawable`
+- [-] maybe LP typealias? does ot solve anything, as we still need to specify generic variant, which
+ would cause name collision
+ ==: yes, `LP` does not bring any benefits only trouble with generics
+- [X] shape-drawable stateful handling
+ ==: In the end changed implementation to `ViewState`, which is a little better abstraction
+ `DrawableState` and all its usages are deprecated
+- [X]
+ > refactor DrawableState to be fluent, right now a little confusing, let it be:
+ `pressed.enabled` => DrawableState(attrs: Array<@Attr Int>)
+ DrawableStateSet.pressed (to check if contains should be renamed, like isPressed or hasPressed?)
+ == in the end done differently
+- [X]] on view pred draw should have `once` as it delivers callback only once
+- [X] element+extensions, accessibility properties
+- [-] common interface for shape and stateful-shape
+ // done by different state builder
+- [X] explicit tags in sample (enum?), define colors for each
- [-] view group, diff, obtain same type and bind if id is different
this would complicate current simple (adn transition-ready) diff, as we would need to lookup
if item is present in the list further, so we can safely reuse it
@@ -52,8 +99,8 @@
- [x] clipToOutline to allow clipping view by using the shape
- [x] castLayout when inside an adapt item is not working, as by default just viewgroup params are
set because by default element-item is using default parameters provided by view-factory
-
Size of adapt-ui release binary
+
- `421KB` with toString and properties
-- `366KB` with static toString in shape and gradient
\ No newline at end of file
+- `366KB` with static toString in shape and gradient
diff --git a/adapt-kt/build.gradle b/adapt-kt/build.gradle
new file mode 100644
index 00000000..2796139c
--- /dev/null
+++ b/adapt-kt/build.gradle
@@ -0,0 +1,48 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.vanniktech.publish)
+}
+
+android {
+ namespace 'io.noties.adapt.kt'
+
+ compileSdkVersion config['target-sdk']
+
+ defaultConfig {
+ minSdkVersion config['min-sdk']
+ targetSdkVersion config['target-sdk']
+ }
+
+ compileOptions {
+ sourceCompatibility config['java-target']
+ targetCompatibility config['java-target']
+ }
+
+ kotlinOptions {
+ jvmTarget = config['java-target']
+ }
+
+ // even if you do not use resources, robolectric still requires this,
+ // otherwise it would skip tests
+ testOptions.unitTests.includeAndroidResources = true
+ lint {
+ fatal 'StopShip'
+ }
+
+}
+
+dependencies {
+ api project(':adapt')
+
+ api libs.androidx.annotations
+
+ testImplementation libs.kotlin.reflect
+ testImplementation libs.junit
+ testImplementation libs.robolectric
+}
+
+mavenPublishing {
+ publishToMavenCentral()
+ signAllPublications()
+}
\ No newline at end of file
diff --git a/adapt-kt/gradle.properties b/adapt-kt/gradle.properties
new file mode 100644
index 00000000..618d6703
--- /dev/null
+++ b/adapt-kt/gradle.properties
@@ -0,0 +1,3 @@
+POM_NAME=AdaptKT
+POM_ARTIFACT_ID=adapt-kt
+POM_PACKAGING=aar
\ No newline at end of file
diff --git a/adapt-kt/src/main/java/io/noties/adapt/kt/AdaptGetter.kt b/adapt-kt/src/main/java/io/noties/adapt/kt/AdaptGetter.kt
new file mode 100644
index 00000000..d80e4452
--- /dev/null
+++ b/adapt-kt/src/main/java/io/noties/adapt/kt/AdaptGetter.kt
@@ -0,0 +1,104 @@
+package io.noties.adapt.kt
+
+import io.noties.adapt.Adapt
+import io.noties.adapt.Item
+
+fun interface AdaptGetter> {
+ fun items(): List
+}
+
+// `abstract` so we can have inline fun with reified generics
+abstract class AdaptGetterBuilder> {
+ abstract fun build(): AdaptGetter
+
+
+ fun filter(test: (T) -> Boolean): AdaptGetterBuilder =
+ Filter(build(), test)
+
+ inline fun filterIsInstance(): AdaptGetterBuilder =
+ FilterIsInstance(build(), R::class.java)
+
+ fun filterIsInstance(type: Class): AdaptGetterBuilder =
+ FilterIsInstance(build(), type)
+
+ /**
+ * Does a direct cast of returned List. NB! the cast will succeed always
+ * but it will throw if on iteration there would be item of different type
+ * (crash at runtime). Performant, as it does not do additional iteration
+ * on items, but potentially fail at runtime if underlying items contain
+ * other type. Use only when underlying collection is known to be on certain type
+ */
+ inline fun cast(): AdaptGetterBuilder =
+ Cast(build(), R::class.java)
+
+ fun cast(type: Class): AdaptGetterBuilder =
+ Cast(build(), type)
+}
+
+fun > Adapt.getter(
+ builder: AdaptGetterBuilder>.() -> AdaptGetterBuilder
+): AdaptGetter {
+ val impl = AdaptGetterBuilderImpl(this)
+ return builder(impl).build()
+}
+
+inline fun > Adapt.getter(): AdaptGetter {
+ return getter { filterIsInstance() }
+}
+
+private class AdaptGetterBuilderImpl(val adapt: Adapt) : AdaptGetterBuilder>() {
+ override fun build(): AdaptGetter> {
+ return AdaptGetter { adapt.items() }
+ }
+}
+
+private class Filter>(
+ val getter: AdaptGetter,
+ val test: (T) -> Boolean
+) : AdaptGetterBuilder() {
+ override fun build(): AdaptGetter {
+ return AdaptGetter {
+ getter.items()
+ .filter(test)
+ }
+ }
+}
+
+@Suppress("FunctionName")
+fun , OUT : IN> FilterIsInstance(
+ getter: AdaptGetter,
+ type: Class
+): AdaptGetterBuilder = FilterIsInstance(Byte.MIN_VALUE, getter, type)
+
+// do not expose this class (it is used by inline, so should have been visible)
+private class FilterIsInstance, OUT : IN>(
+ @Suppress("UNUSED_PARAMETER") dummy: Byte,
+ val getter: AdaptGetter,
+ val type: Class
+) : AdaptGetterBuilder() {
+ override fun build(): AdaptGetter {
+ return AdaptGetter {
+ getter.items()
+ .filterIsInstance(type)
+ }
+ }
+}
+
+@Suppress("FunctionName")
+fun , OUT : IN> Cast(
+ getter: AdaptGetter,
+ type: Class
+): AdaptGetterBuilder = Cast(Byte.MIN_VALUE, getter, type)
+
+private class Cast, OUT : IN>(
+ @Suppress("UNUSED_PARAMETER") dummy: Byte,
+ val getter: AdaptGetter,
+ @Suppress("unused") val type: Class,
+) : AdaptGetterBuilder() {
+ override fun build(): AdaptGetter {
+ return AdaptGetter {
+ @Suppress("UNCHECKED_CAST")
+ getter.items() as List
+ }
+ }
+}
\ No newline at end of file
diff --git a/adapt-kt/src/test/java/io/noties/adapt/kt/AdaptGetterTest.kt b/adapt-kt/src/test/java/io/noties/adapt/kt/AdaptGetterTest.kt
new file mode 100644
index 00000000..f8583d7a
--- /dev/null
+++ b/adapt-kt/src/test/java/io/noties/adapt/kt/AdaptGetterTest.kt
@@ -0,0 +1,290 @@
+package io.noties.adapt.kt
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import io.noties.adapt.Adapt
+import io.noties.adapt.Item
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class AdaptGetterTest {
+
+ @Test
+ fun empty() {
+ val adapt = mockAdapt(emptyList())
+
+ val getter = adapt.getter>()
+ assertEquals(
+ adapt.items(),
+ getter.items()
+ )
+ }
+
+ @Test
+ fun filter() {
+ // 3 instances of each type
+ val adapt = mockAdapt(
+ (0 until 9)
+ .map {
+ val id = it.toLong()
+ when (val rem = it % 3) {
+ 0 -> MockItem1(id)
+ 1 -> MockItem2(id)
+ 2 -> UnrelatedItem(id)
+ else -> error("Unexpected remainder:$rem from:$it")
+ }
+ }
+ )
+
+ // ------------
+ // filter by id
+ // ------------
+
+ // 0, 3, 6
+ run {
+ val getter = adapt.getter {
+ filter { it.id() % 3L == 0L }
+ }
+
+ getter.items()
+ .map { it.id() }
+ .also {
+ assertEquals(listOf(0L, 3L, 6L), it)
+ }
+
+ getter.items()
+ .map { it.javaClass }
+ .also {
+ assertEquals((0 until 3).map { MockItem1::class.java }, it)
+ }
+ }
+
+ // 1, 4, 7
+ run {
+ val getter = adapt.getter {
+ filter { it.id() % 3L == 1L }
+ }
+
+ getter.items()
+ .map { it.id() }
+ .also {
+ assertEquals(listOf(1L, 4L, 7L), it)
+ }
+
+ getter.items()
+ .map { it.javaClass }
+ .also {
+ assertEquals((0 until 3).map { MockItem2::class.java }, it)
+ }
+ }
+
+ // 2, 5, 8
+ run {
+ val getter = adapt.getter {
+ filter { it.id() % 3L == 2L }
+ }
+
+ getter.items()
+ .map { it.id() }
+ .also {
+ assertEquals(listOf(2L, 5L, 8L), it)
+ }
+
+ getter.items()
+ .map { it.javaClass }
+ .also {
+ assertEquals((0 until 3).map { UnrelatedItem::class.java }, it)
+ }
+ }
+
+ // -----
+ // even id, mixture of item types
+ // -----
+ run {
+ val getter = adapt.getter {
+ filter { it.id() % 2L == 0L }
+ }
+
+ assertEquals(
+ listOf(
+ MockItem1(0L),
+ UnrelatedItem(2L),
+ MockItem2(4L),
+ MockItem1(6L),
+ UnrelatedItem(8L)
+ ),
+ getter.items()
+ )
+ }
+ }
+
+ @Test
+ fun filterIsInstance() {
+ val adapt = mockAdapt(
+ (0 until 9L)
+ .map {
+ when (val rem = it % 3L) {
+ 0L -> MockItem1(it)
+ 1L -> MockItem2(it)
+ 2L -> UnrelatedItem(it)
+ else -> error("Unexpected remainder:$rem id:$it")
+ }
+ }
+ )
+
+ // mock-item-1
+ run {
+ val getter = adapt.getter {
+ filterIsInstance()
+ }
+
+ assertEquals(
+ listOf(MockItem1(0L), MockItem1(3L), MockItem1(6L)),
+ getter.items()
+ )
+ }
+
+ // mock-item-2
+ run {
+ val getter = adapt.getter {
+ filterIsInstance()
+ }
+
+ assertEquals(
+ listOf(MockItem2(1L), MockItem2(4L), MockItem2(7L)),
+ getter.items()
+ )
+ }
+
+ // unrelated
+ run {
+ val getter = adapt.getter {
+ filterIsInstance()
+ }
+
+ assertEquals(
+ listOf(UnrelatedItem(2L), UnrelatedItem(5L), UnrelatedItem(8L)),
+ getter.items()
+ )
+ }
+
+ // base-mock
+ run {
+ val getter = adapt.getter {
+ filterIsInstance()
+ }
+
+ assertEquals(
+ listOf(
+ MockItem1(0L),
+ MockItem2(1L),
+ MockItem1(3L),
+ MockItem2(4L),
+ MockItem1(6L),
+ MockItem2(7L),
+ ),
+ getter.items()
+ )
+ }
+ }
+
+ @Test
+ fun cast() {
+ val adapt = mockAdapt(
+ (0 until 9L)
+ .map {
+ when (val rem = it % 3L) {
+ 0L -> MockItem1(it)
+ 1L -> MockItem2(it)
+ 2L -> UnrelatedItem(it)
+ else -> error("Unexpected remainder:$rem id:$it")
+ }
+ }
+ )
+
+ val getter = adapt.getter { this.cast() }
+
+ // cast succeeds, but fails when iterating
+ val items = getter.items()
+
+ try {
+ for (item in items) {
+ item.baseMockItemSpecificProperty
+ }
+ assertTrue(false)
+ } catch (t: Throwable) {
+ assertTrue(true)
+ }
+ }
+
+ private sealed class BaseMockItem(id: Long) : Item(id) {
+ val baseMockItemSpecificProperty: Boolean = true
+
+ private class Holder(view: View) : Item.Holder(view)
+
+ override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
+ error("Stub!!")
+ }
+
+ override fun bind(holder: Holder) = Unit
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is BaseMockItem) return false
+
+ if (id() != other.id()) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return id().hashCode()
+ }
+ }
+
+ private class MockItem1(id: Long) : BaseMockItem(id)
+ private class MockItem2(id: Long) : BaseMockItem(id)
+
+ private class UnrelatedItem(id: Long) : Item(id) {
+ override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
+ error("Stub!!")
+ }
+
+ override fun bind(holder: Holder) = Unit
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is UnrelatedItem) return false
+
+ if (id() != other.id()) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return id().hashCode()
+ }
+ }
+
+ private fun mockAdapt(items: List>): Adapt = object : Adapt {
+
+ val items = items.toMutableList()
+
+ override fun items() = items
+
+ override fun setItems(items: MutableList>?) {
+ this.items.clear()
+ this.items.addAll(items ?: emptyList())
+ }
+
+ override fun notifyAllItemsChanged() = Unit
+ override fun notifyItemChanged(item: Item<*>) = Unit
+
+ override fun registerOnItemsChangedListener(listener: Adapt.OnItemsChangedListener) = Unit
+ override fun unregisterOnItemsChangedListener(listener: Adapt.OnItemsChangedListener) = Unit
+ }
+}
\ No newline at end of file
diff --git a/adapt-ui-flex/build.gradle b/adapt-ui-flex/build.gradle
index 23dbed1d..b6cc5371 100644
--- a/adapt-ui-flex/build.gradle
+++ b/adapt-ui-flex/build.gradle
@@ -1,13 +1,13 @@
plugins {
- id 'com.android.library'
- id 'org.jetbrains.kotlin.android'
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.vanniktech.publish)
}
android {
namespace 'io.noties.adapt.ui.flex'
compileSdkVersion config['target-sdk']
- buildToolsVersion config['build-tools']
defaultConfig {
minSdkVersion config['min-sdk']
@@ -15,37 +15,34 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility config['java-target']
+ targetCompatibility config['java-target']
}
kotlinOptions {
- jvmTarget = '1.8'
- }
-
- publishing {
- singleVariant('release') {
- withSourcesJar()
- }
+ jvmTarget = config['java-target']
}
// even if you do not use resources, robolectric still requires this,
// otherwise it would skip tests
testOptions.unitTests.includeAndroidResources = true
-
- lintOptions {
+ lint {
fatal 'StopShip'
}
+
}
dependencies {
api project(':adapt-ui')
- api deps['x-annotations']
+ api libs.androidx.annotations
- api deps['flexbox']
+ api libs.google.flexbox
testImplementation project(':adapt-ui-test-shared')
}
-apply from: '../publish.gradle'
\ No newline at end of file
+mavenPublishing {
+ publishToMavenCentral()
+ signAllPublications()
+}
\ No newline at end of file
diff --git a/adapt-ui-flex/src/main/java/io/noties/adapt/ui/flex/Flex.kt b/adapt-ui-flex/src/main/java/io/noties/adapt/ui/flex/Flex.kt
index 8d54fdae..2590156d 100644
--- a/adapt-ui-flex/src/main/java/io/noties/adapt/ui/flex/Flex.kt
+++ b/adapt-ui-flex/src/main/java/io/noties/adapt/ui/flex/Flex.kt
@@ -2,18 +2,18 @@ package io.noties.adapt.ui.flex
import android.view.View
import android.view.ViewGroup.LayoutParams
-import com.google.android.flexbox.AlignContent as _AlignContent
-import com.google.android.flexbox.AlignItems as _AlignItems
-import com.google.android.flexbox.AlignSelf as _AlignSelf
-import com.google.android.flexbox.FlexDirection as _FlexDirection
-import com.google.android.flexbox.FlexWrap as _FlexWrap
import com.google.android.flexbox.FlexboxLayout
-import com.google.android.flexbox.JustifyContent as _JustifyContent
import io.noties.adapt.ui.ViewElement
import io.noties.adapt.ui.ViewFactory
import io.noties.adapt.ui.element.ElementGroup
import io.noties.adapt.ui.shape.RectangleShape
import io.noties.adapt.ui.util.dip
+import com.google.android.flexbox.AlignContent as _AlignContent
+import com.google.android.flexbox.AlignItems as _AlignItems
+import com.google.android.flexbox.AlignSelf as _AlignSelf
+import com.google.android.flexbox.FlexDirection as _FlexDirection
+import com.google.android.flexbox.FlexWrap as _FlexWrap
+import com.google.android.flexbox.JustifyContent as _JustifyContent
@Suppress("FunctionName")
fun ViewFactory.Flex(
@@ -28,13 +28,15 @@ fun ViewFactory.Flex(
* Customization of the FlexLayout
*/
@JvmInline
-value class FlexDirection(@_FlexDirection val value: Int) {
+value class FlexDirection(@_FlexDirection val rawValue: Int) {
companion object {
val row: FlexDirection get() = FlexDirection(_FlexDirection.ROW)
val column: FlexDirection get() = FlexDirection(_FlexDirection.COLUMN)
+
+ fun raw(@_FlexDirection value: Int) = FlexDirection(value)
}
- val reverse: FlexDirection get() = if (value == _FlexDirection.COLUMN) {
+ val reverse: FlexDirection get() = if (rawValue == _FlexDirection.COLUMN) {
_FlexDirection.COLUMN_REVERSE
} else {
_FlexDirection.ROW_REVERSE
@@ -44,26 +46,40 @@ value class FlexDirection(@_FlexDirection val value: Int) {
fun ViewElement.flexDirection(
flexDirection: FlexDirection
): ViewElement = onView {
- it.flexDirection = flexDirection.value
+ it.flexDirection = flexDirection.rawValue
+}
+
+fun ViewElement.flexDirection(
+ builder: FlexDirection.Companion.() -> FlexDirection
+): ViewElement = onView {
+ it.flexDirection = builder(FlexDirection).rawValue
}
@JvmInline
-value class FlexWrap(@_FlexWrap val value: Int) {
+value class FlexWrap(@_FlexWrap val rawValue: Int) {
companion object {
val nowrap: FlexWrap get() = FlexWrap(_FlexWrap.NOWRAP)
val wrap: FlexWrap get() = FlexWrap(_FlexWrap.WRAP)
val wrapReverse: FlexWrap get() = FlexWrap(_FlexWrap.WRAP_REVERSE)
+
+ fun raw(@_FlexWrap value: Int) = FlexWrap(value)
}
}
fun ViewElement.flexWrap(
flexWrap: FlexWrap
): ViewElement = onView {
- it.flexWrap = flexWrap.value
+ it.flexWrap = flexWrap.rawValue
+}
+
+fun ViewElement.flexWrap(
+ builder: FlexWrap.Companion.() -> FlexWrap = { wrap }
+): ViewElement = onView {
+ it.flexWrap = builder(FlexWrap).rawValue
}
@JvmInline
-value class JustifyContent(@_JustifyContent val value: Int) {
+value class JustifyContent(@_JustifyContent val rawValue: Int) {
companion object {
val flexStart: JustifyContent get() = JustifyContent(_JustifyContent.FLEX_START)
val flexEnd: JustifyContent get() = JustifyContent(_JustifyContent.FLEX_END)
@@ -71,34 +87,50 @@ value class JustifyContent(@_JustifyContent val value: Int) {
val spaceBetween: JustifyContent get() = JustifyContent(_JustifyContent.SPACE_BETWEEN)
val spaceAround: JustifyContent get() = JustifyContent(_JustifyContent.SPACE_AROUND)
val spaceEvenly: JustifyContent get() = JustifyContent(_JustifyContent.SPACE_EVENLY)
+
+ fun raw(@_JustifyContent value: Int) = JustifyContent(value)
}
}
fun ViewElement.flexJustifyContent(
justifyContent: JustifyContent
): ViewElement = onView {
- it.justifyContent = justifyContent.value
+ it.justifyContent = justifyContent.rawValue
+}
+
+fun ViewElement.flexJustifyContent(
+ builder: JustifyContent.Companion.() -> JustifyContent
+): ViewElement = onView {
+ it.justifyContent = builder(JustifyContent).rawValue
}
@JvmInline
-value class AlignItems(@_AlignItems val value: Int) {
+value class AlignItems(@_AlignItems val rawValue: Int) {
companion object {
val flexStart: AlignItems get() = AlignItems(_AlignItems.FLEX_START)
val flexEnd: AlignItems get() = AlignItems(_AlignItems.FLEX_END)
val center: AlignItems get() = AlignItems(_AlignItems.CENTER)
val baseline: AlignItems get() = AlignItems(_AlignItems.BASELINE)
val stretch: AlignItems get() = AlignItems(_AlignItems.STRETCH)
+
+ fun raw(@_AlignItems value: Int) = AlignItems(value)
}
}
fun ViewElement.flexAlignItems(
alignItems: AlignItems
): ViewElement = onView {
- it.alignItems = alignItems.value
+ it.alignItems = alignItems.rawValue
+}
+
+fun ViewElement.flexAlignItems(
+ builder: AlignItems.Companion.() -> AlignItems
+): ViewElement = onView {
+ it.alignItems = builder(AlignItems).rawValue
}
@JvmInline
-value class AlignContent(@_AlignContent val value: Int) {
+value class AlignContent(@_AlignContent val rawValue: Int) {
companion object {
val flexStart: AlignContent get() = AlignContent(_AlignContent.FLEX_START)
val flexEnd: AlignContent get() = AlignContent(_AlignContent.FLEX_END)
@@ -106,13 +138,21 @@ value class AlignContent(@_AlignContent val value: Int) {
val spaceBetween: AlignContent get() = AlignContent(_AlignContent.SPACE_BETWEEN)
val spaceAround: AlignContent get() = AlignContent(_AlignContent.SPACE_AROUND)
val stretch: AlignContent get() = AlignContent(_AlignContent.STRETCH)
+
+ fun raw(@_AlignContent value: Int) = AlignContent(value)
}
}
fun ViewElement.flexAlignContent(
alignContent: AlignContent
): ViewElement = onView {
- it.alignContent = alignContent.value
+ it.alignContent = alignContent.rawValue
+}
+
+fun ViewElement.flexAlignContent(
+ builder: AlignContent.Companion.() -> AlignContent
+): ViewElement = onView {
+ it.alignContent = builder(AlignContent).rawValue
}
// FlexboxLayout does not have gap support, but we can use a special drawable as divider
@@ -175,7 +215,7 @@ fun ViewElement.layoutFlexShr
}
@JvmInline
-value class AlignSelf(@_AlignSelf val value: Int) {
+value class AlignSelf(@_AlignSelf val rawValue: Int) {
companion object {
val auto: AlignSelf get() = AlignSelf(_AlignSelf.AUTO)
val flexStart: AlignSelf get() = AlignSelf(_AlignItems.FLEX_START)
@@ -183,13 +223,21 @@ value class AlignSelf(@_AlignSelf val value: Int) {
val center: AlignSelf get() = AlignSelf(_AlignItems.CENTER)
val baseline: AlignSelf get() = AlignSelf(_AlignItems.BASELINE)
val stretch: AlignSelf get() = AlignSelf(_AlignItems.STRETCH)
+
+ fun raw(@_AlignSelf value: Int) = AlignSelf(value)
}
}
fun ViewElement.layoutFlexAlignSelf(
alignSelf: AlignSelf
): ViewElement = onLayoutParams {
- it.alignSelf = alignSelf.value
+ it.alignSelf = alignSelf.rawValue
+}
+
+fun ViewElement.layoutFlexAlignSelf(
+ builder: AlignSelf.Companion.() -> AlignSelf
+): ViewElement = onLayoutParams {
+ it.alignSelf = builder(AlignSelf).rawValue
}
fun ViewElement.layoutFlexMinSize(
diff --git a/adapt-ui-flex/src/main/java/io/noties/adapt/ui/flex/FlexElementViewFactory.kt b/adapt-ui-flex/src/main/java/io/noties/adapt/ui/flex/FlexElementViewFactory.kt
index ba818df7..85827ff8 100644
--- a/adapt-ui-flex/src/main/java/io/noties/adapt/ui/flex/FlexElementViewFactory.kt
+++ b/adapt-ui-flex/src/main/java/io/noties/adapt/ui/flex/FlexElementViewFactory.kt
@@ -1,6 +1,7 @@
package io.noties.adapt.ui.flex
import android.content.Context
+import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
object FlexElementViewFactory {
@@ -11,6 +12,11 @@ object FlexElementViewFactory {
}
fun reset() {
- Flex = { FlexboxLayout(it) }
+ Flex = {
+ FlexboxLayout(it).also { fl ->
+ // let it wrap by default
+ fl.flexWrap = FlexWrap.WRAP
+ }
+ }
}
}
\ No newline at end of file
diff --git a/adapt-ui-flex/src/test/java/io/noties/adapt/ui/flex/Flex_Test.kt b/adapt-ui-flex/src/test/java/io/noties/adapt/ui/flex/Flex_Test.kt
index ea3e7790..e7ea37f4 100644
--- a/adapt-ui-flex/src/test/java/io/noties/adapt/ui/flex/Flex_Test.kt
+++ b/adapt-ui-flex/src/test/java/io/noties/adapt/ui/flex/Flex_Test.kt
@@ -49,7 +49,7 @@ class Flex_Test {
for (input in inputs) {
Assert.assertEquals(
input.name,
- input.flexDirection.value,
+ input.flexDirection.rawValue,
input.value
)
}
@@ -59,7 +59,7 @@ class Flex_Test {
fun `flexDirection - unknown`() {
Assert.assertEquals(
_FlexDirection.ROW_REVERSE,
- FlexDirection(777).reverse.value
+ FlexDirection(777).reverse.rawValue
)
}
@@ -76,7 +76,7 @@ class Flex_Test {
newElementOfType()
.flexDirection(input)
.renderView {
- verify(this).flexDirection = eq(input.value)
+ verify(this).flexDirection = eq(input.rawValue)
}
}
}
@@ -92,7 +92,7 @@ class Flex_Test {
for ((flexWrap, value) in inputs) {
Assert.assertEquals(
flexWrap.name,
- flexWrap.get().value,
+ flexWrap.get().rawValue,
value
)
}
@@ -111,7 +111,7 @@ class Flex_Test {
newElementOfType()
.flexWrap(input)
.renderView {
- verify(this).flexWrap = eq(input.value)
+ verify(this).flexWrap = eq(input.rawValue)
}
}
}
@@ -130,7 +130,7 @@ class Flex_Test {
for ((prop, value) in inputs) {
Assert.assertEquals(
prop.name,
- prop.get().value,
+ prop.get().rawValue,
value
)
}
@@ -152,7 +152,7 @@ class Flex_Test {
newElementOfType()
.flexJustifyContent(input)
.renderView {
- verify(this).justifyContent = eq(input.value)
+ verify(this).justifyContent = eq(input.rawValue)
}
}
}
@@ -170,7 +170,7 @@ class Flex_Test {
for ((prop, value) in inputs) {
Assert.assertEquals(
prop.name,
- prop.get().value,
+ prop.get().rawValue,
value
)
}
@@ -191,7 +191,7 @@ class Flex_Test {
newElementOfType()
.flexAlignItems(input)
.renderView {
- verify(this).alignItems = eq(input.value)
+ verify(this).alignItems = eq(input.rawValue)
}
}
}
@@ -210,7 +210,7 @@ class Flex_Test {
for ((prop, value) in inputs) {
Assert.assertEquals(
prop.name,
- prop.get().value,
+ prop.get().rawValue,
value
)
}
@@ -232,7 +232,7 @@ class Flex_Test {
newElementOfType()
.flexAlignContent(input)
.renderView {
- verify(this).alignContent = eq(input.value)
+ verify(this).alignContent = eq(input.rawValue)
}
}
}
@@ -342,7 +342,7 @@ class Flex_Test {
for ((prop, value) in inputs) {
Assert.assertEquals(
prop.name,
- prop.get().value,
+ prop.get().rawValue,
value
)
}
@@ -364,7 +364,7 @@ class Flex_Test {
newFlexboxChild()
.layoutFlexAlignSelf(input)
.renderWithLayoutParams {
- verify(it).alignSelf = eq(input.value)
+ verify(it).alignSelf = eq(input.rawValue)
}
}
}
diff --git a/adapt-ui-test-shared/build.gradle b/adapt-ui-test-shared/build.gradle
index 9c3ee59e..68432956 100644
--- a/adapt-ui-test-shared/build.gradle
+++ b/adapt-ui-test-shared/build.gradle
@@ -1,13 +1,12 @@
plugins {
- id 'com.android.library'
- id 'org.jetbrains.kotlin.android'
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
}
android {
namespace 'io.noties.adapt.ui.test'
compileSdkVersion config['target-sdk']
- buildToolsVersion config['build-tools']
defaultConfig {
minSdkVersion config['min-sdk']
@@ -15,12 +14,12 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility config['java-target']
+ targetCompatibility config['java-target']
}
kotlinOptions {
- jvmTarget = '1.8'
+ jvmTarget = config['java-target']
}
// even if you do not use resources, robolectric still requires this,
@@ -31,8 +30,8 @@ android {
dependencies {
api project(':adapt-ui')
- api deps['junit4']
- api deps['mockito']
- api deps['mockito-kotlin']
- api deps['robolectric']
+ api libs.junit
+ api libs.mockito.core
+ api libs.mockito.kotlin
+ api libs.robolectric
}
\ No newline at end of file
diff --git a/adapt-ui/README.md b/adapt-ui/README.md
index c3077b35..d65c5cef 100644
--- a/adapt-ui/README.md
+++ b/adapt-ui/README.md
@@ -1,7 +1,7 @@
## AdaptUI
-
+
🚧 All showcase previews can be accessed via [dedicate page](../PREVIEW_SHOWCASE.md)
@@ -56,7 +56,7 @@ val view = ViewFactory.newView(viewGroup)
// but it is possible to postpone rendering until view is attached to window
.renderOnAttach()
.create {
- // root view will receive passed LayoutParams(12, 66)
+ // root view will receive passed LayoutParams(12, 66)
HStack {
// just a generic View
View()
@@ -82,7 +82,7 @@ val view = ViewFactory.newView(viewGroup)
* `ZStack` -> `android.widget.FrameLayout`
#### Composite elements:
-* `HScrollStack` -> `HScroll { HStack { /*children*/ } }`
+* `HScrollStack` -> `HScroll { HStack { /*children*/ } }`
* `VScrollStack` -> `VScroll { VStack { /*children*/ } }`
#### Special elements:
@@ -190,8 +190,8 @@ Image()
.elevation(4)
```
-If some functionality is not covered by provided extensions `onView` allows getting
-access to created view directly:
+If some functionality is not covered by provided
+extensions `onView` allows getting access to created view directly:
```kotlin
Text()
@@ -214,8 +214,8 @@ fun ViewElement.activated(
// looses original types, BEWARE - limits fluent usage
fun ViewElement.activated2(
activated: Boolean = true
-) = onView {
- it.isActivated = activated
+) = onView {
+ it.isActivated = activated
}
ViewFactory.createView(context) {
@@ -290,7 +290,7 @@ fun ViewElement.textPrimary() = this
Text()
.textPrimary()
// still possible to customize it further
- // overrides value from style extension
+ // overrides value from style extension
.textSize(21)
```
diff --git a/adapt-ui/build.gradle b/adapt-ui/build.gradle
index 7fd751c8..17b6f705 100644
--- a/adapt-ui/build.gradle
+++ b/adapt-ui/build.gradle
@@ -1,15 +1,13 @@
plugins {
- id 'com.android.library'
- id 'org.jetbrains.kotlin.android'
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.vanniktech.publish)
}
android {
- // https://developer.android.com/studio/publish-library/prep-lib-release
- // What this namespace represents? It doesn't seem to be used anywhere
namespace 'io.noties.adapt.ui'
compileSdkVersion config['target-sdk']
- buildToolsVersion config['build-tools']
defaultConfig {
minSdkVersion config['min-sdk']
@@ -17,44 +15,41 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility config['java-target']
+ targetCompatibility config['java-target']
}
kotlinOptions {
- jvmTarget = '1.8'
- }
-
- publishing {
- singleVariant('release') {
- withSourcesJar()
- }
+ jvmTarget = config['java-target']
}
// even if you do not use resources, robolectric still requires this,
// otherwise it would skip tests
testOptions.unitTests.includeAndroidResources = true
-
- lintOptions {
+ lint {
fatal 'StopShip'
}
+
}
dependencies {
api project(':adapt')
- api deps['x-annotations']
+ api libs.androidx.annotations
- compileOnly deps['x-recyclerview']
- compileOnly deps['x-viewpager']
- compileOnly deps['x-viewpager2']
+ compileOnly libs.androidx.recyclerview
+ compileOnly libs.androidx.viewpager
+ compileOnly libs.androidx.viewpager2
testImplementation project(':adapt-ui-test-shared')
- testImplementation deps['x-recyclerview']
- testImplementation deps['x-viewpager']
- testImplementation deps['x-viewpager2']
- testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
+ testImplementation libs.androidx.recyclerview
+ testImplementation libs.androidx.viewpager
+ testImplementation libs.androidx.viewpager2
+ testImplementation libs.kotlin.reflect
}
-apply from: '../publish.gradle'
\ No newline at end of file
+mavenPublishing {
+ publishToMavenCentral()
+ signAllPublications()
+}
\ No newline at end of file
diff --git a/adapt-ui/src/main/AndroidManifest.xml b/adapt-ui/src/main/AndroidManifest.xml
index 1d26c87a..5f18fbac 100644
--- a/adapt-ui/src/main/AndroidManifest.xml
+++ b/adapt-ui/src/main/AndroidManifest.xml
@@ -1,2 +1,9 @@
-
\ No newline at end of file
+
+
+
+
+
\ No newline at end of file
diff --git a/adapt-ui/src/main/java/io/noties/adapt/ui/LayoutParams.kt b/adapt-ui/src/main/java/io/noties/adapt/ui/LayoutParams.kt
new file mode 100644
index 00000000..8d1e673c
--- /dev/null
+++ b/adapt-ui/src/main/java/io/noties/adapt/ui/LayoutParams.kt
@@ -0,0 +1,5 @@
+package io.noties.adapt.ui
+
+import android.view.ViewGroup
+
+typealias LayoutParams = ViewGroup.LayoutParams
\ No newline at end of file
diff --git a/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+Extensions.kt b/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+Extensions.kt
index d8d31954..90d7c5b6 100644
--- a/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+Extensions.kt
+++ b/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+Extensions.kt
@@ -1,5 +1,6 @@
package io.noties.adapt.ui
+import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.SystemClock
@@ -9,8 +10,15 @@ import android.view.View.VISIBLE
import android.view.ViewTreeObserver
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
import androidx.annotation.FloatRange
-import androidx.annotation.RequiresApi
+import io.noties.adapt.ui.app.color.Colors
+import io.noties.adapt.ui.app.color.ColorsBuilder
+import io.noties.adapt.ui.app.dimen.Dimens
+import io.noties.adapt.ui.app.dimen.DimensBuilder
+import io.noties.adapt.ui.gradient.Gradient
+import io.noties.adapt.ui.gradient.GradientBuilder
+import io.noties.adapt.ui.shape.Rectangle
import io.noties.adapt.ui.shape.Shape
import io.noties.adapt.ui.shape.ShapeFactory
import io.noties.adapt.ui.shape.ShapeFactoryBuilder
@@ -92,12 +100,37 @@ fun ViewElement.tag(
* Background
* @see View.setBackgroundColor
*/
+@Deprecated(
+ "Use `backgroundColor` instead",
+ replaceWith = ReplaceWith(
+ "backgroundColor(color)",
+ imports = ["io.noties.adapt.ui.backgroundColor"]
+ )
+)
fun ViewElement.background(
@ColorInt color: Int
+) = backgroundColor(color)
+
+/**
+ * Background
+ * @see View.setBackgroundColor
+ */
+fun ViewElement.backgroundColor(
+ @ColorInt color: Int
): ViewElement = onView {
it.setBackgroundColor(color)
}
+/**
+ * Background that receives [Colors] instance to provide named color values
+ * ```kotlin
+ * Text()
+ * .background { main }
+ * ```
+ */
+inline fun ViewElement.backgroundColor(builder: ColorsBuilder) =
+ backgroundColor(builder(Colors))
+
/**
* @see View.setBackground
*/
@@ -122,6 +155,17 @@ fun ViewElement.background(
block: ShapeFactoryBuilder
): ViewElement = background(block(ShapeFactory.NoOp))
+/**
+ * @see View.setBackground
+ */
+fun ViewElement.backgroundGradient(
+ gradient: GradientBuilder
+): ViewElement = background {
+ Rectangle {
+ fill(gradient(Gradient))
+ }
+}
+
/**
* @see background(Int)
* @see background(Drawable?)
@@ -131,18 +175,47 @@ fun ViewElement.backgroundDefaultSelectable
it.background = resolveDefaultSelectableDrawable(it.context)
}
+/**
+ * @see backgroundResource(Int)
+ * @see background(Drawable?)
+ */
+fun ViewElement.backgroundResource(
+ @DrawableRes id: Int
+): ViewElement = onView {
+ it.setBackgroundResource(id)
+}
+
+
+/**
+ * Foreground color
+ */
+fun ViewElement.foregroundColor(
+ @ColorInt color: Int
+) = foreground(ColorDrawable(color))
+
+/**
+ * Foreground color
+ * ```kotlin
+ * View()
+ * .foregroundColor { main }
+ * ```
+ * @see Colors
+ */
+inline fun ViewElement.foregroundColor(
+ builder: ColorsBuilder
+) = foregroundColor(builder(Colors))
+
/**
* Foreground
* @see View.setForeground
* @see View.setForegroundGravity
*/
-@RequiresApi(Build.VERSION_CODES.M)
fun ViewElement.foreground(
drawable: Drawable?,
gravity: Gravity? = null
): ViewElement = onView { view ->
view.foreground = drawable
- gravity?.also { view.foregroundGravity = it.value }
+ gravity?.also { view.foregroundGravity = it.rawValue }
}
/**
@@ -150,7 +223,6 @@ fun ViewElement.foreground(
* @see View.setForegroundGravity
* @see Shape.newDrawable
*/
-@RequiresApi(Build.VERSION_CODES.M)
fun ViewElement.foreground(
shape: Shape,
gravity: Gravity? = null
@@ -162,7 +234,6 @@ fun ViewElement.foreground(
* @see Shape.newDrawable
* @see ShapeFactory
*/
-@RequiresApi(Build.VERSION_CODES.M)
fun ViewElement.foreground(
gravity: Gravity? = null,
block: ShapeFactoryBuilder
@@ -171,7 +242,6 @@ fun ViewElement.foreground(
/**
* @see View.setForeground
*/
-@RequiresApi(Build.VERSION_CODES.M)
fun ViewElement.foregroundDefaultSelectable(): ViewElement =
onView {
it.foreground = resolveDefaultSelectableDrawable(it.context)
@@ -244,7 +314,7 @@ fun ViewElement.selected(
* @see View.setVisibility
*/
fun ViewElement.visible(
- visible: Boolean
+ visible: Boolean = true
): ViewElement = onView {
it.visibility = if (visible) VISIBLE else GONE
}
@@ -303,6 +373,18 @@ fun ViewElement.elevation(
it.elevation = elevation.dip.toFloat()
}
+/**
+ * Elevation
+ * ```kotlin
+ * View()
+ * .elevation { elevationLight }
+ * ```
+ * @see View.setElevation
+ */
+inline fun ViewElement.elevation(
+ builder: DimensBuilder
+) = elevation(builder(Dimens))
+
/**
* Translation
* @see View.setTranslationX
@@ -385,26 +467,82 @@ fun ViewElement.onViewScrollChanged(
}
}
+@JvmInline
+value class OverScrollMode(val rawValue: Int) {
+ companion object {
+ val never: OverScrollMode get() = OverScrollMode(View.OVER_SCROLL_NEVER)
+ val always: OverScrollMode get() = OverScrollMode(View.OVER_SCROLL_ALWAYS)
+ val ifContentScrolls: OverScrollMode get() = OverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS)
+
+ fun raw(rawValue: Int) = OverScrollMode(rawValue)
+ }
+}
+
/**
* OverScrollMode
* @see View.setOverScrollMode
*/
+fun ViewElement.overScrollMode(
+ builder: OverScrollMode.Companion.() -> OverScrollMode
+) = onView {
+ it.overScrollMode = builder(OverScrollMode).rawValue
+}
+
+/**
+ * OverScrollMode
+ * @see View.setOverScrollMode
+ */
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated("Consider using overScrollMode builder version")
fun ViewElement.overScrollMode(
overScrollMode: Int
): ViewElement = onView {
it.overScrollMode = overScrollMode
}
+@JvmInline
+value class ScrollBarStyle(val rawValue: Int) {
+ companion object {
+ val insideOverlay: ScrollBarStyle get() = ScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY)
+ val insideInset: ScrollBarStyle get() = ScrollBarStyle(View.SCROLLBARS_INSIDE_INSET)
+ val outsideOverlay: ScrollBarStyle get() = ScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY)
+ val outsideInset: ScrollBarStyle get() = ScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET)
+
+ fun raw(value: Int) = ScrollBarStyle(value)
+ }
+}
+
/**
* ScrollBarStyle
* @see View.setScrollBarStyle
*/
+fun ViewElement.scrollBarStyle(
+ builder: ScrollBarStyle.Companion.() -> ScrollBarStyle
+): ViewElement = onView {
+ it.scrollBarStyle = builder(ScrollBarStyle).rawValue
+}
+
+/**
+ * ScrollBarStyle
+ * @see View.setScrollBarStyle
+ */
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated("Prefer the builder version")
fun ViewElement.scrollBarStyle(
scrollBarStyle: Int
): ViewElement = onView {
it.scrollBarStyle = scrollBarStyle
}
+/**
+ * Scroll bars
+ * @see View.setHorizontalScrollBarEnabled
+ * @see View.setVerticalScrollBarEnabled
+ */
+fun ViewElement.scrollBarsEnabled(
+ value: Boolean
+) = scrollBarsEnabled(horizontal = value, vertical = value)
+
/**
* Scroll bars
* @see View.setHorizontalScrollBarEnabled
@@ -519,8 +657,6 @@ fun ViewElement.onViewPreDrawOnce(
unregisterOnPreDraw()
}
-// TODO: use normal and unregister on first event
-
/**
* NB! This is a callback when view is attached to [android.view.Window], not its parent
*/
@@ -686,3 +822,8 @@ fun ViewElement.pivotRelative(
value: Float
) = pivotRelative(value, value)
+fun ViewElement.requestFocus() = onView {
+ it.requestFocus()
+}
+
+
diff --git a/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+ExtensionsAccessibility.kt b/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+ExtensionsAccessibility.kt
index ef0aecaf..34363d6a 100644
--- a/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+ExtensionsAccessibility.kt
+++ b/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+ExtensionsAccessibility.kt
@@ -1,9 +1,7 @@
package io.noties.adapt.ui
-import android.os.Build
import android.view.View
import androidx.annotation.IdRes
-import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
/**
@@ -26,7 +24,11 @@ fun ViewElement.accessibilityDescription(
}
@JvmInline
-value class ImportantForAccessibility(val value: Int) {
+value class ImportantForAccessibility(val rawValue: Int) {
+
+ @Deprecated("Use `rawValue`", ReplaceWith("rawValue"))
+ val value: Int get() = rawValue
+
companion object {
val yes: ImportantForAccessibility get() = ImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES)
val no: ImportantForAccessibility get() = ImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO)
@@ -49,9 +51,16 @@ fun ViewElement.accessibilityImportant(
fun ViewElement.accessibilityImportant(
importantForAccessibility: ImportantForAccessibility
): ViewElement = onView {
- it.importantForAccessibility = importantForAccessibility.value
+ it.importantForAccessibility = importantForAccessibility.rawValue
}
+/**
+ * @see View.setImportantForAccessibility
+ */
+fun ViewElement.accessibilityImportant(
+ builder: ImportantForAccessibility.Companion.() -> ImportantForAccessibility
+): ViewElement = accessibilityImportant(builder(ImportantForAccessibility))
+
/**
* @see View.setLabelFor
*/
@@ -75,7 +84,6 @@ fun ViewElement.accessibilityLabelFor(
/**
* @see View.setAccessibilityTraversalBefore
*/
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
fun ViewElement.accessibilityTraversalBefore(
@IdRes targetId: Int,
): ViewElement = onView {
@@ -85,7 +93,6 @@ fun ViewElement.accessibilityTraversalBefor
/**
* @see View.setAccessibilityTraversalBefore
*/
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
fun ViewElement.accessibilityTraversalBefore(
provider: () -> ViewElement,
): ViewElement = this.also {
@@ -97,7 +104,6 @@ fun ViewElement.accessibilityTraversalBefor
/**
* @see View.setAccessibilityTraversalAfter
*/
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
fun ViewElement.accessibilityTraversalAfter(
@IdRes targetId: Int,
): ViewElement = onView {
@@ -107,7 +113,6 @@ fun ViewElement.accessibilityTraversalAfter
/**
* @see View.setAccessibilityTraversalAfter
*/
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
fun ViewElement.accessibilityTraversalAfter(
provider: () -> ViewElement,
): ViewElement = this.also {
@@ -117,7 +122,11 @@ fun ViewElement.accessibilityTraversalAfter
}
@JvmInline
-value class AccessibilityLiveRegion(val value: Int) {
+value class AccessibilityLiveRegion(val rawValue: Int) {
+
+ @Deprecated("Use `rawValue`", ReplaceWith("rawValue"))
+ val value: Int get() = rawValue
+
companion object {
val none: AccessibilityLiveRegion get() = AccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE)
val polite: AccessibilityLiveRegion get() = AccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE)
@@ -131,9 +140,16 @@ value class AccessibilityLiveRegion(val value: Int) {
fun ViewElement.accessibilityLiveRegion(
liveRegion: AccessibilityLiveRegion,
): ViewElement = onView {
- it.accessibilityLiveRegion = liveRegion.value
+ it.accessibilityLiveRegion = liveRegion.rawValue
}
+/**
+ * @see View.setAccessibilityLiveRegion
+ */
+fun ViewElement.accessibilityLiveRegion(
+ builder: AccessibilityLiveRegion.Companion.() -> AccessibilityLiveRegion,
+): ViewElement = accessibilityLiveRegion(builder(AccessibilityLiveRegion))
+
// if view has id use it, otherwise generate a new id (and assign it to the view)
private fun View.ensureId(): Int = this.id
diff --git a/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+ExtensionsCast.kt b/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+ExtensionsCast.kt
index d04c2520..21e797fd 100644
--- a/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+ExtensionsCast.kt
+++ b/adapt-ui/src/main/java/io/noties/adapt/ui/ViewElement+ExtensionsCast.kt
@@ -11,9 +11,9 @@ import kotlin.reflect.KClass
* Please note that if element is not yet initialized, casting would happen
* @see ifCastView
*/
-fun ViewElement.castView(
+fun ViewElement.castView(
klass: KClass,
-): ViewElement {
+): ViewElement {
fun matches() = klass.java.isAssignableFrom(this.view::class.java)
fun deliver(cause: Throwable?) {
@@ -39,18 +39,18 @@ fun ViewElement.castView(
}
@Suppress("UNCHECKED_CAST")
- return this as ViewElement
+ return this as ViewElement
}
-fun ViewElement.ifCastView(
+fun ViewElement.ifCastView(
klass: KClass,
- block: (ViewElement) -> Unit
+ block: (ViewElement) -> Unit
): ViewElement {
fun matches() = klass.java.isAssignableFrom(this.view::class.java)
fun deliver() {
@Suppress("UNCHECKED_CAST")
- block(this as ViewElement)
+ block(this as ViewElement)
render()
}
@@ -72,9 +72,9 @@ fun ViewElement.ifCastView(
//inline fun ViewElement.castLayout(
//): ViewElement where RLP : LP = castLayout<_, _, _>(RLP::class)
-fun ViewElement.castLayout(
+fun ViewElement.castLayout(
klass: KClass
-): ViewElement where RLP : LP {
+): ViewElement {
fun matches(): Boolean = klass.java.isAssignableFrom(view.layoutParams::class.java)
fun deliver(cause: Throwable?) {
throw AdaptClassCastException(
@@ -99,18 +99,18 @@ fun