diff --git a/.github/workflows/compilation-check.yml b/.github/workflows/compilation-check.yml index 0352a1b1..5c4a9c54 100644 --- a/.github/workflows/compilation-check.yml +++ b/.github/workflows/compilation-check.yml @@ -23,4 +23,4 @@ jobs: - name: Cocoapods install run: (cd sample/ios-app && pod install) - name: Build pods-dependent and publish local - run: ./gradlew -PlibraryPublish :widgets-flat:publishToMavenLocal :widgets-bottomsheet:publishToMavenLocal :widgets-sms:publishToMavenLocal \ No newline at end of file + run: ./gradlew -PlibraryPublish :widgets-flat:publishToMavenLocal :widgets-bottomsheet:publishToMavenLocal :widgets-sms:publishToMavenLocal :widgets-datetime-picker:publishToMavenLocal :widgets-collection:publishToMavenLocal \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bd45f705..007056cf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,4 +21,4 @@ jobs: - name: Cocoapods install run: (cd sample/ios-app && pod install) - name: Build pods-dependent and publish local - run: ./gradlew -PlibraryPublish :widgets-flat:publishAllPublicationsToBintrayRepository :widgets-bottomsheet:publishAllPublicationsToBintrayRepository :widgets-sms:publishAllPublicationsToBintrayRepository -DBINTRAY_USER=${{ secrets.BINTRAY_USER }} -DBINTRAY_KEY=${{ secrets.BINTRAY_KEY }} \ No newline at end of file + run: ./gradlew -PlibraryPublish :widgets-flat:publishAllPublicationsToBintrayRepository :widgets-bottomsheet:publishAllPublicationsToBintrayRepository :widgets-sms:publishAllPublicationsToBintrayRepository :widgets-datetime-picker:publishAllPublicationsToBintrayRepository :widgets-collection:publishAllPublicationsToBintrayRepository -DBINTRAY_USER=${{ secrets.BINTRAY_USER }} -DBINTRAY_KEY=${{ secrets.BINTRAY_KEY }} \ No newline at end of file diff --git a/README.md b/README.md index e64f210d..5f69d91d 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This is a Kotlin MultiPlatform library that provides declarative UI and applicat in common code. You can implement full application for Android and iOS only from common code with it. ## Current status -Current version - `0.1.0-dev-14`. Dev version is not tested in production tasks yet, API can be changed and +Current version - `0.1.0-dev-15`. Dev version is not tested in production tasks yet, API can be changed and bugs may be found. But dev version is chance to test limits of API and concepts to feedback and improve lib. We open for any feedback and ideas (go to issues or #moko at [kotlinlang.slack.com](https://kotlinlang.slack.com))! @@ -222,6 +222,7 @@ val loginScreen = Theme(baseTheme) { - 0.1.0-dev-12 - 0.1.0-dev-13 - 0.1.0-dev-14 + - 0.1.0-dev-15 ## Installation root build.gradle @@ -236,7 +237,7 @@ allprojects { project build.gradle ```groovy dependencies { - commonMainApi("dev.icerock.moko:widgets:0.1.0-dev-14") + commonMainApi("dev.icerock.moko:widgets:0.1.0-dev-15") } ``` @@ -254,7 +255,7 @@ buildscript { } dependencies { - classpath "dev.icerock.moko.widgets:gradle-plugin:0.1.0-dev-14" + classpath "dev.icerock.moko.widgets:gradle-plugin:0.1.0-dev-15" } } @@ -370,19 +371,18 @@ Please see more examples in the [sample directory](sample). - The [widgets directory](widgets) contains the `widgets` library; - The [widgets-bottomsheet directory](widgets-bottomsheet) contains the `widgets-bottomsheet` library; - The [widgets-sms directory](widgets-sms) contains the `widgets-sms` library; +- The [widgets-datetime-picker directory](widgets-datetime-picker) contains the `datetime-picker` library; +- The [widgets-collection directory](widgets-collection) contains the `collection` library; - The [gradle-plugin directory](gradle-plugin) contains the gradle-plugin which apply compiler plugins for Native and JVM; - The [kotlin-plugin directory](kotlin-plugin) contains the JVM compiler plugin with code-generation from @WidgetDef annotation; - The [kotlin-native-plugin directory](kotlin-native-plugin) contains the Native compiler plugin with code-generation from @WidgetDef annotation; - The [kotlin-common-plugin directory](kotlin-common-plugin) contains the common code of JVM and Native compiler plugins; - The [sample directory](sample) contains sample apps for Android and iOS; plus the mpp-library connected to the apps; - For local testing a library use: - - `./gradlew -PpluginPublish publishPluginPublicationToMavenLocal` - - `./gradlew -PlibraryPublish :widgets:publishToMavenLocal` - - `cd sample/ios-app && pod install` - - `./gradlew -PlibraryPublish :widgets-flat:publishToMavenLocal :widgets-bottomsheet:publishToMavenLocal :widgets-sms:publishToMavenLocal` + - `./publishToMavenLocal.sh` + - `cd sample/ios-app` + - `pod install` - sample apps priority use the locally published version - - `./gradlew :sample:mpp-library:syncMultiPlatformLibraryDebugFrameworkIosX64` - compile sample shared code for iOS - - `cd sample/ios-app && pod install` - install pods with compiled shared code - run android from `Android Studio` - module `android-app`, run iOS from xcode workspace `sample/ios-app/ios-app.xcworkspace` ## Contributing diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 1d00a8cc..e80c75b1 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - implementation("dev.icerock:mobile-multiplatform:0.5.2") + implementation("dev.icerock:mobile-multiplatform:0.5.3") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61") implementation("com.android.tools.build:gradle:3.5.2") } diff --git a/buildSrc/src/main/kotlin/Deps.kt b/buildSrc/src/main/kotlin/Deps.kt index 67b13fc1..eb6b5395 100755 --- a/buildSrc/src/main/kotlin/Deps.kt +++ b/buildSrc/src/main/kotlin/Deps.kt @@ -73,6 +73,16 @@ object Deps { iosX64 = "dev.icerock.moko:widgets-bottomsheet-iosx64:${Versions.Libs.MultiPlatform.mokoWidgets}", iosArm64 = "dev.icerock.moko:widgets-bottomsheet-iosarm64:${Versions.Libs.MultiPlatform.mokoWidgets}" ) + val mokoWidgetsCollection = MultiPlatformLibrary( + common = "dev.icerock.moko:widgets-collection:${Versions.Libs.MultiPlatform.mokoWidgets}", + iosX64 = "dev.icerock.moko:widgets-collection-iosx64:${Versions.Libs.MultiPlatform.mokoWidgets}", + iosArm64 = "dev.icerock.moko:widgets-collection-iosarm64:${Versions.Libs.MultiPlatform.mokoWidgets}" + ) + val mokoWidgetsDateTimePicker = MultiPlatformLibrary( + common = "dev.icerock.moko:widgets-datetime-picker:${Versions.Libs.MultiPlatform.mokoWidgets}", + iosX64 = "dev.icerock.moko:widgets-datetime-picker-iosx64:${Versions.Libs.MultiPlatform.mokoWidgets}", + iosArm64 = "dev.icerock.moko:widgets-datetime-picker-iosarm64:${Versions.Libs.MultiPlatform.mokoWidgets}" + ) val mokoResources = MultiPlatformLibrary( common = "dev.icerock.moko:resources:${Versions.Libs.MultiPlatform.mokoResources}", iosX64 = "dev.icerock.moko:resources-iosx64:${Versions.Libs.MultiPlatform.mokoResources}", diff --git a/moko-widgets-collection.podspec b/moko-widgets-collection.podspec new file mode 100644 index 00000000..40a89b27 --- /dev/null +++ b/moko-widgets-collection.podspec @@ -0,0 +1,20 @@ +Pod::Spec.new do |spec| + spec.name = 'moko-widgets-collection' + spec.version = '0.1.0' + spec.homepage = 'https://github.com/icerockdev/moko-widgets' + spec.source = { :git => "https://github.com/icerockdev/moko-widgets.git", :tag => "release/#{spec.version}" } + spec.authors = 'IceRock Development' + spec.license = { :type => 'Apache 2', :file => 'LICENSE.md' } + spec.summary = 'Swift additions to moko-widgets Kotlin/Native library' + spec.module_name = "mokoWidgetsCollection" + + spec.source_files = "widgets-collection/src/iosMain/swift/**/*.{h,m,swift}" + spec.resources = "widgets-collection/src/iosMain/bundle/**/*" + + spec.ios.deployment_target = '11.0' + spec.swift_version = '5.0' + + spec.pod_target_xcconfig = { + 'VALID_ARCHS' => '$(ARCHS_STANDARD_64_BIT)' + } +end diff --git a/publishToMavenLocal.sh b/publishToMavenLocal.sh index 13637d78..efd4b26f 100755 --- a/publishToMavenLocal.sh +++ b/publishToMavenLocal.sh @@ -1,3 +1,4 @@ ./gradlew -PpluginPublish publishPluginPublicationToMavenLocal ./gradlew -PlibraryPublish :widgets:publishToMavenLocal -./gradlew -PlibraryPublish :widgets-flat:publishToMavenLocal :widgets-bottomsheet:publishToMavenLocal :widgets-sms:publishToMavenLocal \ No newline at end of file +(cd sample/ios-app && pod install) +./gradlew -PlibraryPublish :widgets-flat:publishToMavenLocal :widgets-bottomsheet:publishToMavenLocal :widgets-sms:publishToMavenLocal :widgets-datetime-picker:publishToMavenLocal :widgets-collection:publishToMavenLocal \ No newline at end of file diff --git a/sample/ios-app/Podfile b/sample/ios-app/Podfile index fb086090..00932cb9 100644 --- a/sample/ios-app/Podfile +++ b/sample/ios-app/Podfile @@ -17,6 +17,5 @@ target 'TestProj' do pod 'mppLibraryIos', :path => '../mpp-library' pod 'moko-widgets-flat', :path => '../../' pod 'moko-widgets-bottomsheet', :path => '../../' - - pod 'MultiPlatformLibraryMvvm', :git => 'https://github.com/icerockdev/moko-mvvm.git', :tag => 'release/0.4.0-dev-2' + pod 'moko-widgets-collection', :path => '../../' end diff --git a/sample/ios-app/Podfile.lock b/sample/ios-app/Podfile.lock index 49c62bfa..88f00f85 100644 --- a/sample/ios-app/Podfile.lock +++ b/sample/ios-app/Podfile.lock @@ -1,76 +1,48 @@ PODS: - - Alamofire (4.9.1) - - AlamofireImage (3.6.0): - - Alamofire (~> 4.9) - FloatingPanel (1.7.2) - InputMask (5.0.0) - moko-widgets-bottomsheet (0.1.0): - FloatingPanel + - moko-widgets-collection (0.1.0) - moko-widgets-flat (0.1.0): - InputMask (~> 5.0.0) - mppLibraryIos (0.1.0) - MultiPlatformLibrary (0.1.0) - - MultiPlatformLibraryMvvm (0.3.0): - - MultiPlatformLibrary - - MultiPlatformLibraryMvvm/AlamofireImage (= 0.3.0) - - MultiPlatformLibraryMvvm/Core (= 0.3.0) - - MultiPlatformLibraryMvvm/SkyFloatingLabelTextField (= 0.3.0) - - MultiPlatformLibraryMvvm/AlamofireImage (0.3.0): - - AlamofireImage - - MultiPlatformLibrary - - MultiPlatformLibraryMvvm/Core (0.3.0): - - MultiPlatformLibrary - - MultiPlatformLibraryMvvm/SkyFloatingLabelTextField (0.3.0): - - MultiPlatformLibrary - - SkyFloatingLabelTextField - - SkyFloatingLabelTextField (3.7.0) DEPENDENCIES: - moko-widgets-bottomsheet (from `../../`) + - moko-widgets-collection (from `../../`) - moko-widgets-flat (from `../../`) - mppLibraryIos (from `../mpp-library`) - MultiPlatformLibrary (from `../mpp-library`) - - MultiPlatformLibraryMvvm (from `https://github.com/icerockdev/moko-mvvm.git`, tag `release/0.4.0-dev-2`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: - - Alamofire - - AlamofireImage - InputMask - - SkyFloatingLabelTextField trunk: - FloatingPanel EXTERNAL SOURCES: moko-widgets-bottomsheet: :path: "../../" + moko-widgets-collection: + :path: "../../" moko-widgets-flat: :path: "../../" mppLibraryIos: :path: "../mpp-library" MultiPlatformLibrary: :path: "../mpp-library" - MultiPlatformLibraryMvvm: - :git: https://github.com/icerockdev/moko-mvvm.git - :tag: release/0.4.0-dev-2 - -CHECKOUT OPTIONS: - MultiPlatformLibraryMvvm: - :git: https://github.com/icerockdev/moko-mvvm.git - :tag: release/0.4.0-dev-2 SPEC CHECKSUMS: - Alamofire: 85e8a02c69d6020a0d734f6054870d7ecb75cf18 - AlamofireImage: be9963c6582d68b39e89191f64c82a7d7bf40fdd FloatingPanel: b275a35d0a09be4bd37025e710a6a1d063bfc161 InputMask: 8a10dbc8ac3f94f0a5b4c380424bbe6795c69b16 moko-widgets-bottomsheet: 68e942940b15bf0c6605675140d907fdb79db31d + moko-widgets-collection: 722c6fca0b0dcab0b54fdb674b4638d4f715434c moko-widgets-flat: 3d68acddc0469a1288ede5b5b63b6588de5aa5e1 mppLibraryIos: 72c3984fbaa53978d678e62096fd613e67839f0c MultiPlatformLibrary: 176fb8ade516666cd47e93de1b71ba0441a541bb - MultiPlatformLibraryMvvm: 999ac3896d8214fd65e0e0a376a2e553bf4515b3 - SkyFloatingLabelTextField: 4b46db0ab1ccde0919cded29c656e6b4805eda04 -PODFILE CHECKSUM: 4b3a5440fa9c919597e5ebac2ced886871ffb124 +PODFILE CHECKSUM: d67011d3954d166daed6874ad60b67569d84c8b3 -COCOAPODS: 1.8.4 +COCOAPODS: 1.9.0 diff --git a/sample/ios-app/src/Info.plist b/sample/ios-app/src/Info.plist index 52507931..5df5f5c2 100755 --- a/sample/ios-app/src/Info.plist +++ b/sample/ios-app/src/Info.plist @@ -48,6 +48,8 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index b46e2182..582fe510 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -31,12 +31,11 @@ val deps = listOf( Deps.Libs.MultiPlatform.mokoGraphics, Deps.Libs.MultiPlatform.mokoWidgets, Deps.Libs.MultiPlatform.mokoWidgetsFlat, - Deps.Libs.MultiPlatform.mokoWidgetsBottomSheet + Deps.Libs.MultiPlatform.mokoWidgetsBottomSheet, + Deps.Libs.MultiPlatform.mokoWidgetsCollection ) -setupFramework( - exports = deps -) +setupFramework(exports = emptyList()) dependencies { mppLibrary(Deps.Libs.MultiPlatform.kotlinStdLib) @@ -58,5 +57,6 @@ cocoaPods { pod("moko-widgets-flat", "mokoWidgetsFlat", onlyLink = true) pod("moko-widgets-bottomsheet", "mokoWidgetsBottomSheet", onlyLink = true) + pod("moko-widgets-collection", "mokoWidgetsCollection", onlyLink = true) pod("mppLibraryIos") } diff --git a/sample/mpp-library/src/commonMain/kotlin/App.kt b/sample/mpp-library/src/commonMain/kotlin/App.kt index aef8e01c..3cffae26 100644 --- a/sample/mpp-library/src/commonMain/kotlin/App.kt +++ b/sample/mpp-library/src/commonMain/kotlin/App.kt @@ -5,6 +5,8 @@ import com.icerockdev.library.AppTheme import com.icerockdev.library.MR import com.icerockdev.library.SharedFactory +import com.icerockdev.library.sample.PostsScreen +import com.icerockdev.library.sample.PostsViewModel import com.icerockdev.library.universal.CartScreen import com.icerockdev.library.universal.InfoWebViewScreen import com.icerockdev.library.universal.LoginScreen @@ -24,6 +26,8 @@ import dev.icerock.moko.widgets.ImageWidget import dev.icerock.moko.widgets.InputWidget import dev.icerock.moko.widgets.TabsWidget import dev.icerock.moko.widgets.button +import dev.icerock.moko.widgets.collection.CollectionWidget +import dev.icerock.moko.widgets.collection.SimpleCollectionViewFactory import dev.icerock.moko.widgets.container import dev.icerock.moko.widgets.core.Theme import dev.icerock.moko.widgets.core.Value @@ -36,6 +40,8 @@ import dev.icerock.moko.widgets.factory.SystemInputViewFactory import dev.icerock.moko.widgets.factory.SystemTabsViewFactory import dev.icerock.moko.widgets.factory.SystemTextViewFactory import dev.icerock.moko.widgets.flat.FlatInputViewFactory +import dev.icerock.moko.widgets.sample.CollectionImageUnitItem +import dev.icerock.moko.widgets.sample.CollectionScreen import dev.icerock.moko.widgets.sample.InputWidgetGalleryScreen import dev.icerock.moko.widgets.sample.ProductsSearchScreen import dev.icerock.moko.widgets.sample.ScrollContentScreen @@ -45,6 +51,7 @@ import dev.icerock.moko.widgets.screen.Args import dev.icerock.moko.widgets.screen.BaseApplication import dev.icerock.moko.widgets.screen.Screen import dev.icerock.moko.widgets.screen.ScreenDesc +import dev.icerock.moko.widgets.screen.TemplateScreen import dev.icerock.moko.widgets.screen.TypedScreenDesc import dev.icerock.moko.widgets.screen.WidgetScreen import dev.icerock.moko.widgets.screen.navigation.BottomNavigationItem @@ -60,6 +67,7 @@ import dev.icerock.moko.widgets.screen.navigation.createRouter import dev.icerock.moko.widgets.screen.navigation.route import dev.icerock.moko.widgets.style.background.Background import dev.icerock.moko.widgets.style.background.Fill +import dev.icerock.moko.widgets.style.background.Orientation import dev.icerock.moko.widgets.style.state.PressableState import dev.icerock.moko.widgets.style.state.SelectableState import dev.icerock.moko.widgets.style.view.MarginValues @@ -108,6 +116,8 @@ class App() : BaseApplication() { buildInputGalleryRouteInfo(theme, router), buildSearchRouteInfo(theme, router), buildTabsRouteInfo(theme, router), + buildCollectionRouteInfo(theme, router), + buildPostsRouteInfo(theme, router), SelectGalleryScreen.RouteInfo( name = "Old Demo".desc(), route = router.createPushRoute(oldDemo(router)) @@ -177,6 +187,53 @@ class App() : BaseApplication() { ) } + private fun buildCollectionRouteInfo( + theme: Theme, + router: NavigationScreen.Router + ): SelectGalleryScreen.RouteInfo { + val collectionTheme = Theme(theme) { + factory[CollectionWidget.DefaultCategory] = SimpleCollectionViewFactory( + orientation = Orientation.HORIZONTAL, + margins = MarginValues(top = 16f, bottom = 16f) + ) + factory[CollectionImageUnitItem.Id.Image] = SystemImageViewFactory( + cornerRadius = 8f, + margins = MarginValues(start = 4f, end = 4f) + ) + } + val collectionScreen = registerScreen(CollectionScreen::class) { + CollectionScreen(collectionTheme) + } + + return SelectGalleryScreen.RouteInfo( + name = "Collection in list".desc(), + route = router.createPushRoute(collectionScreen) + ) + } + + private fun buildPostsRouteInfo( + theme: Theme, + router: NavigationScreen.Router + ): SelectGalleryScreen.RouteInfo { + val postsTheme = Theme(theme) { + factory[PostsScreen.Id.Collection] = SimpleCollectionViewFactory( + padding = PaddingValues(4f) + ) + } + + val postsScreen = registerScreen(PostsScreen::class) { + PostsScreen( + postsTheme, + PostsViewModel() + ) + } + + return SelectGalleryScreen.RouteInfo( + name = "Posts Collection".desc(), + route = router.createPushRoute(postsScreen) + ) + } + private fun oldDemo( router: NavigationScreen.Router ): TypedScreenDesc { @@ -230,6 +287,10 @@ class App() : BaseApplication() { val mainScreen = registerScreen(MainBottomNavigationScreen::class) { val bottomRouter = createRouter() + val templateScreen = registerScreen(TemplateScreen::class) { + TemplateScreen(navTitle = "Template".desc(), labelText = "Template Screen".desc(), theme = theme) + } + val cartNavigation = registerScreen(CartNavigationScreen::class) { val navigationRouter = createRouter() val profileScreen = registerScreen(PlatformProfileScreen::class) { @@ -306,6 +367,11 @@ class App() : BaseApplication() { title = "Logout".desc(), screenDesc = logoutScreen ) + tab( + id = 5, + title = "Empty".desc(), + screenDesc = templateScreen + ) } } diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/AppTheme.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/AppTheme.kt index d8b76d4b..b6927e4d 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/AppTheme.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/AppTheme.kt @@ -9,16 +9,16 @@ import com.icerockdev.library.sample.UsersScreen import com.icerockdev.library.universal.LoginScreen import dev.icerock.moko.graphics.Color import dev.icerock.moko.widgets.ButtonWidget -import dev.icerock.moko.widgets.CollectionWidget import dev.icerock.moko.widgets.ConstraintWidget import dev.icerock.moko.widgets.InputWidget import dev.icerock.moko.widgets.StatefulWidget +import dev.icerock.moko.widgets.collection.CollectionWidget +import dev.icerock.moko.widgets.collection.SimpleCollectionViewFactory import dev.icerock.moko.widgets.core.Theme import dev.icerock.moko.widgets.factory.ConstraintViewFactory import dev.icerock.moko.widgets.factory.FloatingLabelInputViewFactory import dev.icerock.moko.widgets.factory.StatefulViewFactory import dev.icerock.moko.widgets.factory.SystemButtonViewFactory -import dev.icerock.moko.widgets.factory.SystemCollectionViewFactory import dev.icerock.moko.widgets.factory.SystemListViewFactory import dev.icerock.moko.widgets.factory.SystemTextViewFactory import dev.icerock.moko.widgets.style.background.Background @@ -110,7 +110,7 @@ object AppTheme { ) ) - factory[PostsCollection] = SystemCollectionViewFactory( + factory[PostsCollection] = SimpleCollectionViewFactory( padding = PaddingValues(4f) ) } diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/sample/PostsSample.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/sample/PostsSample.kt index ee393b56..c7d7bbaf 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/sample/PostsSample.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/sample/PostsSample.kt @@ -12,24 +12,26 @@ import dev.icerock.moko.mvvm.viewmodel.ViewModel import dev.icerock.moko.resources.desc.StringDesc import dev.icerock.moko.resources.desc.desc import dev.icerock.moko.units.CollectionUnitItem -import dev.icerock.moko.widgets.CollectionWidget -import dev.icerock.moko.widgets.collection +import dev.icerock.moko.widgets.collection.CollectionWidget +import dev.icerock.moko.widgets.collection.collection import dev.icerock.moko.widgets.core.Theme import dev.icerock.moko.widgets.core.Widget +import dev.icerock.moko.widgets.screen.Args.Empty +import dev.icerock.moko.widgets.screen.WidgetScreen +import dev.icerock.moko.widgets.screen.navigation.NavigationBar +import dev.icerock.moko.widgets.screen.navigation.NavigationItem import dev.icerock.moko.widgets.style.view.SizeSpec import dev.icerock.moko.widgets.style.view.WidgetSize class PostsScreen( private val theme: Theme, - private val viewModel: PostsViewModelContract, - private val collectionCategory: CollectionWidget.Category -) { + private val viewModel: PostsViewModelContract +) : WidgetScreen(), NavigationItem { fun createWidget(): Widget> { return with(theme) { collection( size = WidgetSize.AsParent, id = Id.Collection, - category = collectionCategory, items = viewModel.posts.map { posts -> posts.map { post -> PostCollectionUnitItem( @@ -46,8 +48,16 @@ class PostsScreen( object Id { object Collection : CollectionWidget.Id } + + override val navigationBar: NavigationBar + get() = NavigationBar.Normal("Posts".desc()) + + override fun createContentWidget(): Widget> { + return createWidget() + } } + interface PostsViewModelContract { val posts: LiveData> diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/sample/UsersSample.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/sample/UsersSample.kt index 249f6670..ae160f5f 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/sample/UsersSample.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/sample/UsersSample.kt @@ -11,10 +11,9 @@ import dev.icerock.moko.mvvm.livedata.mergeWith import dev.icerock.moko.mvvm.viewmodel.ViewModel import dev.icerock.moko.units.CollectionUnitItem import dev.icerock.moko.units.TableUnitItem -import dev.icerock.moko.widgets.CollectionWidget import dev.icerock.moko.widgets.ListWidget -import dev.icerock.moko.widgets.TabsWidget -import dev.icerock.moko.widgets.collection +import dev.icerock.moko.widgets.collection.CollectionWidget +import dev.icerock.moko.widgets.collection.collection import dev.icerock.moko.widgets.core.Theme import dev.icerock.moko.widgets.core.Widget import dev.icerock.moko.widgets.list diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/LoadingUnitWidget.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/LoadingUnitWidget.kt index 381df3a3..65751a4b 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/LoadingUnitWidget.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/LoadingUnitWidget.kt @@ -64,8 +64,8 @@ class LoadingUnitWidget( override val reuseId: String = "LoadingUnitItem" - override fun createWidget(data: LiveData): UnitItemRoot { - return unitWidget.createWidget(data).let { UnitItemRoot.from(it) } + override fun createWidget(data: LiveData): Widget { + return unitWidget.createWidget(data) } } } diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/PostCollectionUnitItem.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/PostCollectionUnitItem.kt index 52d5f560..2294cade 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/PostCollectionUnitItem.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/PostCollectionUnitItem.kt @@ -7,6 +7,7 @@ package com.icerockdev.library.units import com.icerockdev.library.sample.PostsViewModelContract import dev.icerock.moko.mvvm.livedata.LiveData import dev.icerock.moko.mvvm.livedata.map +import dev.icerock.moko.widgets.ImageWidget import dev.icerock.moko.widgets.clickable import dev.icerock.moko.widgets.container import dev.icerock.moko.widgets.core.Image @@ -27,49 +28,19 @@ class PostCollectionUnitItem( ) : WidgetsCollectionUnitItem(itemId, data) { override val reuseId: String = "PostUnitItem" - override fun createWidget(data: LiveData): UnitItemRoot { - return with(theme) { - UnitItemRoot.from(createBody(data)) - } + override fun createWidget(data: LiveData): Widget { + return theme.createBody(data) } private fun Theme.createBody(data: LiveData) = clickable( - child = container( -// factory = DefaultContainerWidgetViewFactory( -// DefaultContainerWidgetViewFactoryBase.Style( -// background = Background( -// fill = Fill.Solid(Color(0x66, 0x66, 0x66, 0xFF)) -// ), -// margins = MarginValues(4f) -// ) -// ), - size = WidgetSize.AspectByWidth( - width = SizeSpec.AsParent, - aspectRatio = 0.73f - ) - ) { - center { - image( - size = WidgetSize.Const( - width = SizeSpec.AsParent, - height = SizeSpec.AsParent - ), -// factory = DefaultImageWidgetViewFactory( -// DefaultImageWidgetViewFactoryBase.Style( -// scaleType = DefaultImageWidgetViewFactoryBase.ScaleType.FILL -// ) -// ), - image = data.map { Image.network(it.imageUrl) } - ) - } - top { - createHeader(data) - } - bottom { - createFooter(data) - } - }, + child = image(size = WidgetSize.AspectByWidth( + width = SizeSpec.AsParent, + aspectRatio = 1.5f + ), + scaleType = ImageWidget.ScaleType.FILL, + image = data.map { Image.network(it.imageUrl) } + ), onClick = { println("item $data pressed!") } @@ -91,7 +62,7 @@ class PostCollectionUnitItem( // padding = PaddingValues(bottom = 8f) // ) // ), - size = WidgetSize.Const(SizeSpec.AsParent, SizeSpec.WrapContent) + size = WidgetSize.Const(SizeSpec.MatchConstraint, SizeSpec.MatchConstraint) ) { center { text( @@ -115,7 +86,7 @@ class PostCollectionUnitItem( ): Widget { val regularItems = listOf>( text( - size = WidgetSize.Const(SizeSpec.AsParent, SizeSpec.WrapContent), + size = WidgetSize.Const(SizeSpec.MatchConstraint, SizeSpec.WrapContent), // factory = DefaultTextWidgetViewFactory( // DefaultTextWidgetViewFactoryBase.Style( // textStyle = TextStyle( diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/UserUnitWidget.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/UserUnitWidget.kt index 91487d84..b997c4ec 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/UserUnitWidget.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/units/UserUnitWidget.kt @@ -11,6 +11,7 @@ import dev.icerock.moko.resources.desc.desc import dev.icerock.moko.widgets.clickable import dev.icerock.moko.widgets.core.Image import dev.icerock.moko.widgets.core.Theme +import dev.icerock.moko.widgets.core.Widget import dev.icerock.moko.widgets.image import dev.icerock.moko.widgets.linear import dev.icerock.moko.widgets.style.background.Orientation @@ -109,8 +110,8 @@ class UserUnitWidget( override val reuseId: String = "UserUnitItem" - override fun createWidget(data: LiveData): UnitItemRoot { - return unitWidget.createWidget(data).let { UnitItemRoot.from(it) } + override fun createWidget(data: LiveData): Widget { + return unitWidget.createWidget(data) } } } diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/universal/WidgetsScreen.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/universal/WidgetsScreen.kt index db16113c..b140a7fd 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/universal/WidgetsScreen.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/universal/WidgetsScreen.kt @@ -17,7 +17,7 @@ import com.icerockdev.library.sample.StateScreen import com.icerockdev.library.sample.StateViewModel import com.icerockdev.library.sample.UsersScreen import com.icerockdev.library.sample.UsersViewModel -import dev.icerock.moko.widgets.CollectionWidget +import dev.icerock.moko.widgets.collection.CollectionWidget import dev.icerock.moko.widgets.constraint import dev.icerock.moko.widgets.core.Theme import dev.icerock.moko.widgets.core.Widget @@ -81,8 +81,7 @@ class WidgetsScreen( title = const("P"), body = PostsScreen( theme = theme, - viewModel = PostsViewModel(), - collectionCategory = postsCollectionCategory + viewModel = PostsViewModel() ).createWidget() ) tab( diff --git a/sample/mpp-library/src/commonMain/kotlin/dev/icerock/moko/widgets/sample/CollectionScreen.kt b/sample/mpp-library/src/commonMain/kotlin/dev/icerock/moko/widgets/sample/CollectionScreen.kt new file mode 100644 index 00000000..997c2524 --- /dev/null +++ b/sample/mpp-library/src/commonMain/kotlin/dev/icerock/moko/widgets/sample/CollectionScreen.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.sample + +import dev.icerock.moko.mvvm.livedata.LiveData +import dev.icerock.moko.mvvm.livedata.MutableLiveData +import dev.icerock.moko.mvvm.livedata.map +import dev.icerock.moko.resources.desc.desc +import dev.icerock.moko.units.CollectionUnitItem +import dev.icerock.moko.units.TableUnitItem +import dev.icerock.moko.widgets.ImageWidget +import dev.icerock.moko.widgets.ListWidget +import dev.icerock.moko.widgets.collection.CollectionWidget +import dev.icerock.moko.widgets.collection.collection +import dev.icerock.moko.widgets.constraint +import dev.icerock.moko.widgets.core.Image +import dev.icerock.moko.widgets.core.Theme +import dev.icerock.moko.widgets.core.Widget +import dev.icerock.moko.widgets.image +import dev.icerock.moko.widgets.list +import dev.icerock.moko.widgets.screen.Args +import dev.icerock.moko.widgets.screen.WidgetScreen +import dev.icerock.moko.widgets.screen.navigation.NavigationBar +import dev.icerock.moko.widgets.screen.navigation.NavigationItem +import dev.icerock.moko.widgets.style.view.SizeSpec +import dev.icerock.moko.widgets.style.view.WidgetSize +import dev.icerock.moko.widgets.units.UnitItemRoot +import dev.icerock.moko.widgets.units.WidgetsCollectionUnitItem +import dev.icerock.moko.widgets.units.WidgetsTableUnitItem + +class CollectionScreen( + private val theme: Theme +) : WidgetScreen(), NavigationItem { + + override val navigationBar: NavigationBar = NavigationBar.Normal(title = "Collection in list".desc()) + + override fun createContentWidget(): Widget> { + return with(theme) { + constraint(size = WidgetSize.AsParent) { + + val content = +content() + + constraints { + content topToTop root.safeArea + content leftRightToLeftRight root + content bottomToBottom root.safeArea + } + } + } + } + + private fun Theme.content() = list( + size = WidgetSize.Const( + width = SizeSpec.AsParent, + height = SizeSpec.MatchConstraint + ), + id = Ids.List, + items = listItems() + ) + + private fun Theme.listItems() = List>(10) { + listOf( + "https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885__340.jpg", + "https://cdn.pixabay.com/photo/2015/02/24/15/41/dog-647528__340.jpg", + "https://images.pexels.com/photos/814499/pexels-photo-814499.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500", + "https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2016/03/1458289957powerful-images3.jpg" + ) + }.mapIndexed { index, urls -> + ImageSliderUnit( + itemId = index.toLong(), + data = urls.map { Image.network(it) }, + theme = this + ) as TableUnitItem + }.let { MutableLiveData(it) } + + object Ids { + object List : ListWidget.Id + } +} + +class ImageSliderUnit( + itemId: Long, + data: List, + private val theme: Theme +) : WidgetsTableUnitItem>(itemId, data) { + override val reuseId: String = "ImageSliderUnit" + + override fun createWidget(data: LiveData>): UnitItemRoot { + return UnitItemRoot.from(theme.createUnitWidget(data)) + } + + private fun Theme.createUnitWidget(data: LiveData>) = collection( + size = WidgetSize.Const(SizeSpec.AsParent, SizeSpec.Exact(182f)), + id = Id.Collection, + items = data.map { + it.mapIndexed { index, image -> + CollectionImageUnitItem( + itemId = index.toLong(), + data = image, + theme = theme + ) as CollectionUnitItem + } + } + ) + + object Id { + object Collection : CollectionWidget.Id + } +} + +class CollectionImageUnitItem( + itemId: Long, + data: Image, + private val theme: Theme +) : WidgetsCollectionUnitItem(itemId, data) { + override val reuseId: String = "CollectionImageUnitItem" + + override fun createWidget(data: LiveData): Widget { + return with(theme) { + image( + size = WidgetSize.Const(SizeSpec.Exact(312f), SizeSpec.Exact(182f)), + id = Id.Image, + scaleType = ImageWidget.ScaleType.FILL, + image = data + ) + } + } + + object Id { + object Image : ImageWidget.Id + } +} diff --git a/sample/mpp-library/src/commonMain/resources/MR/base/strings.xml b/sample/mpp-library/src/commonMain/resources/MR/base/strings.xml index cffafd71..c569894f 100644 --- a/sample/mpp-library/src/commonMain/resources/MR/base/strings.xml +++ b/sample/mpp-library/src/commonMain/resources/MR/base/strings.xml @@ -1,6 +1,6 @@ - test + test string test 2 test 3 Test Project diff --git a/sample/mpp-library/src/commonMain/resources/MR/ru/strings.xml b/sample/mpp-library/src/commonMain/resources/MR/ru/strings.xml index ac2042f7..fbd3376d 100644 --- a/sample/mpp-library/src/commonMain/resources/MR/ru/strings.xml +++ b/sample/mpp-library/src/commonMain/resources/MR/ru/strings.xml @@ -1,6 +1,6 @@ - тест + тестовая строка тест 2 тест 3 Тестовый проект diff --git a/settings.gradle.kts b/settings.gradle.kts index 754cd9a8..8366666a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -42,6 +42,8 @@ if (!pluginPublish) { include(":widgets-flat") include(":widgets-sms") include(":widgets-bottomsheet") + include(":widgets-collection") + include(":widgets-datetime-picker") if (!libraryPublish) { include(":sample:android-app") diff --git a/widgets-bottomsheet/src/androidMain/kotlin/dev/icerock/moko/widgets/bottomsheet/BottomSheet.kt b/widgets-bottomsheet/src/androidMain/kotlin/dev/icerock/moko/widgets/bottomsheet/BottomSheet.kt index f459e596..f8841513 100644 --- a/widgets-bottomsheet/src/androidMain/kotlin/dev/icerock/moko/widgets/bottomsheet/BottomSheet.kt +++ b/widgets-bottomsheet/src/androidMain/kotlin/dev/icerock/moko/widgets/bottomsheet/BottomSheet.kt @@ -4,6 +4,7 @@ package dev.icerock.moko.widgets.bottomsheet +import android.content.Context import com.google.android.material.bottomsheet.BottomSheetDialog import dev.icerock.moko.widgets.core.ViewFactoryContext import dev.icerock.moko.widgets.core.Widget @@ -13,10 +14,10 @@ import dev.icerock.moko.widgets.style.view.WidgetSize actual fun Screen<*>.showBottomSheet( content: Widget>, - onDismiss: () -> Unit -) { - val context = context ?: return - val dialog = BottomSheetDialog(context) + onDismiss: (isSelfDismissed: Boolean) -> Unit +): SelfDismisser? { + val context = context ?: return null + val dialog = DismissedBottomSheetDialog(context, onDismiss) dialog.setContentView( content.buildView( ViewFactoryContext( @@ -26,6 +27,14 @@ actual fun Screen<*>.showBottomSheet( ) ).view ) - dialog.setOnCancelListener { onDismiss() } + dialog.setOnCancelListener { onDismiss(false) } dialog.show() + return dialog } + +private class DismissedBottomSheetDialog(context: Context, val onDismiss: (Boolean) -> Unit): BottomSheetDialog(context), SelfDismisser { + override fun dismissSelf() { + this.dismiss() + onDismiss(true) + } +} \ No newline at end of file diff --git a/widgets-bottomsheet/src/commonMain/kotlin/dev/icerock/moko/widgets/bottomsheet/BottomSheet.kt b/widgets-bottomsheet/src/commonMain/kotlin/dev/icerock/moko/widgets/bottomsheet/BottomSheet.kt index 3532025e..28032164 100644 --- a/widgets-bottomsheet/src/commonMain/kotlin/dev/icerock/moko/widgets/bottomsheet/BottomSheet.kt +++ b/widgets-bottomsheet/src/commonMain/kotlin/dev/icerock/moko/widgets/bottomsheet/BottomSheet.kt @@ -11,5 +11,9 @@ import dev.icerock.moko.widgets.style.view.WidgetSize expect fun Screen<*>.showBottomSheet( content: Widget>, - onDismiss: () -> Unit -) + onDismiss: (isSelfDismissed: Boolean) -> Unit +): SelfDismisser? + +interface SelfDismisser { + fun dismissSelf() +} \ No newline at end of file diff --git a/widgets-bottomsheet/src/iosMain/kotlin/dev/icerock/moko/widgets/bottomSheet/BottomSheet.kt b/widgets-bottomsheet/src/iosMain/kotlin/dev/icerock/moko/widgets/bottomSheet/BottomSheet.kt index d907fb3d..43638ab4 100644 --- a/widgets-bottomsheet/src/iosMain/kotlin/dev/icerock/moko/widgets/bottomSheet/BottomSheet.kt +++ b/widgets-bottomsheet/src/iosMain/kotlin/dev/icerock/moko/widgets/bottomSheet/BottomSheet.kt @@ -12,12 +12,22 @@ import dev.icerock.moko.widgets.style.view.WidgetSize actual fun Screen<*>.showBottomSheet( content: Widget>, - onDismiss: () -> Unit -) { + onDismiss: (isSelfDismissed: Boolean) -> Unit +): SelfDismisser? { val view = content.buildView(viewController).view - BottomSheetController().showOnViewController( + val holder = BottomSheetHolder() + holder.bottomSheet.showOnViewController( vc = this.viewController, withContent = view, onDismiss = onDismiss ) + return holder } + +private class BottomSheetHolder: SelfDismisser { + val bottomSheet = BottomSheetController() + + override fun dismissSelf() { + bottomSheet.dismiss() + } +} \ No newline at end of file diff --git a/widgets-bottomsheet/src/iosMain/swift/BottomSheetController.swift b/widgets-bottomsheet/src/iosMain/swift/BottomSheetController.swift index 8b32554b..bf0ffa1f 100644 --- a/widgets-bottomsheet/src/iosMain/swift/BottomSheetController.swift +++ b/widgets-bottomsheet/src/iosMain/swift/BottomSheetController.swift @@ -9,17 +9,20 @@ private var AssociatedDelegateHandle: UInt8 = 0 @objc public class BottomSheetController: NSObject, FloatingPanelControllerDelegate { + private weak var controller: FloatingPanelController? + private var onDismiss: ((Bool) -> Void)? + @objc public func show( onViewController vc: UIViewController, withContent view: UIView, - onDismiss: @escaping () -> Void + onDismiss: @escaping (Bool) -> Void ) { view.updateConstraints() view.layoutSubviews() - + let maxSize = CGSize(width: UIScreen.main.bounds.width, height: UIView.layoutFittingCompressedSize.height) view.frame = UIScreen.main.bounds - + let floatLayout = BottomSheetLayout( preferredHeight: view.systemLayoutSizeFitting( UIView.layoutFittingCompressedSize, @@ -40,17 +43,29 @@ private var AssociatedDelegateHandle: UInt8 = 0 fpc.backdropView.backgroundColor = UIColor.black fpc.isRemovalInteractionEnabled = true + view.superview?.layer.maskedCorners = [CACornerMask.layerMinXMinYCorner, CACornerMask.layerMaxXMinYCorner] + view.superview?.layer.masksToBounds = true + view.superview?.layer.cornerRadius = 14 + + controller = fpc + self.onDismiss = onDismiss + objc_setAssociatedObject(fpc, &AssociatedDelegateHandle, delegate, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) vc.present(fpc, animated: true, completion: nil) } + + @objc public func dismiss() { + controller?.dismiss(animated: true, completion: nil) + self.onDismiss?(true) + } } class FloatingDelegate: FloatingPanelControllerDelegate { private let floatingLayout: FloatingPanelLayout - private let onDismiss: () -> Void + private let onDismiss: (Bool) -> Void - init(floatingLayout: FloatingPanelLayout, onDismiss: @escaping () -> Void) { + init(floatingLayout: FloatingPanelLayout, onDismiss: @escaping (Bool) -> Void) { self.floatingLayout = floatingLayout self.onDismiss = onDismiss } @@ -59,8 +74,16 @@ class FloatingDelegate: FloatingPanelControllerDelegate { return floatingLayout } + func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) { + if vc.position == .hidden { + vc.removePanelFromParent(animated: true) + vc.dismiss(animated: false, completion: nil) + onDismiss(false) + } + } + func floatingPanelDidEndRemove(_ vc: FloatingPanelController) { - onDismiss() + onDismiss(false) } } @@ -78,11 +101,11 @@ class BottomSheetLayout: FloatingPanelLayout { case .half: return preferredHeight case .full: return 0 case .tip: return 0 - case .hidden: return nil + case .hidden: return 0 } } - var supportedPositions: Set = [.half, .tip] + var supportedPositions: Set = [.half, .hidden] func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat { return 0.3 diff --git a/widgets-collection/build.gradle.kts b/widgets-collection/build.gradle.kts new file mode 100644 index 00000000..7890dc08 --- /dev/null +++ b/widgets-collection/build.gradle.kts @@ -0,0 +1,55 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.multiplatform") + id("dev.icerock.mobile.multiplatform") + id("maven-publish") + id("dev.icerock.mobile.multiplatform-widgets-generator") +} + +group = "dev.icerock.moko" +version = Versions.Libs.MultiPlatform.mokoWidgets + +android { + compileSdkVersion(Versions.Android.compileSdk) + + defaultConfig { + minSdkVersion(Versions.Android.minSdk) + targetSdkVersion(Versions.Android.targetSdk) + } +} + +dependencies { + mppLibrary(Deps.Libs.MultiPlatform.kotlinStdLib) + mppLibrary(Deps.Libs.MultiPlatform.coroutines) + + mppLibrary(Deps.Libs.MultiPlatform.mokoMvvm) + mppLibrary(Deps.Libs.MultiPlatform.mokoResources) + mppLibrary(Deps.Libs.MultiPlatform.mokoFields) + mppLibrary(Deps.Libs.MultiPlatform.mokoWidgets) + mppLibrary(Deps.Libs.MultiPlatform.mokoUnits) + + androidLibrary(Deps.Libs.Android.lifecycle) + androidLibrary(Deps.Libs.Android.recyclerView) + androidLibrary(Deps.Libs.Android.swipeRefreshLayout) +} + +publishing { + repositories.maven("https://api.bintray.com/maven/icerockdev/moko/moko-widgets/;publish=1") { + name = "bintray" + + credentials { + username = System.getProperty("BINTRAY_USER") + password = System.getProperty("BINTRAY_KEY") + } + } +} + +cocoaPods { + podsProject = file("../sample/ios-app/Pods/Pods.xcodeproj") + + pod(scheme = "moko-widgets-collection", module = "mokoWidgetsCollection") +} diff --git a/widgets-collection/src/androidMain/AndroidManifest.xml b/widgets-collection/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..d9349e2d --- /dev/null +++ b/widgets-collection/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemCollectionViewFactory.kt b/widgets-collection/src/androidMain/kotlin/dev/icerock/moko/widgets/collection/SimpleCollectionViewFactory.kt similarity index 68% rename from widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemCollectionViewFactory.kt rename to widgets-collection/src/androidMain/kotlin/dev/icerock/moko/widgets/collection/SimpleCollectionViewFactory.kt index 8b7718b7..1d085312 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemCollectionViewFactory.kt +++ b/widgets-collection/src/androidMain/kotlin/dev/icerock/moko/widgets/collection/SimpleCollectionViewFactory.kt @@ -1,14 +1,16 @@ /* - * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.widgets.factory +package dev.icerock.moko.widgets.collection +import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import dev.icerock.moko.units.UnitItem import dev.icerock.moko.units.adapter.UnitsRecyclerViewAdapter -import dev.icerock.moko.widgets.CollectionWidget import dev.icerock.moko.widgets.core.View import dev.icerock.moko.widgets.core.ViewBundle import dev.icerock.moko.widgets.core.ViewFactory @@ -18,17 +20,14 @@ import dev.icerock.moko.widgets.style.applyPaddingIfNeeded import dev.icerock.moko.widgets.style.background.Background import dev.icerock.moko.widgets.style.background.Fill import dev.icerock.moko.widgets.style.background.Orientation -import dev.icerock.moko.widgets.style.ext.toStaggeredGridLayoutManager import dev.icerock.moko.widgets.style.view.MarginValues import dev.icerock.moko.widgets.style.view.PaddingValues import dev.icerock.moko.widgets.style.view.WidgetSize import dev.icerock.moko.widgets.utils.androidId import dev.icerock.moko.widgets.utils.bind -import dev.icerock.moko.widgets.view.UnitItemDecorator -actual class SystemCollectionViewFactory actual constructor( +actual class SimpleCollectionViewFactory actual constructor( private val orientation: Orientation, - private val spanCount: Int, private val padding: PaddingValues?, private val margins: MarginValues?, private val background: Background? @@ -48,7 +47,7 @@ actual class SystemCollectionViewFactory actual constructor( val recyclerView = RecyclerView(context).apply { clipToPadding = false layoutManager = StaggeredGridLayoutManager( - spanCount, + 1, // in columned collection will be changed orientation.toStaggeredGridLayoutManager() ) adapter = unitsAdapter @@ -56,7 +55,7 @@ actual class SystemCollectionViewFactory actual constructor( id = widget.id.androidId applyPaddingIfNeeded(padding) - applyBackgroundIfNeeded(this@SystemCollectionViewFactory.background) + applyBackgroundIfNeeded(this@SimpleCollectionViewFactory.background) } val resultView: View = if (haveSwipeRefreshListener) { @@ -79,11 +78,12 @@ actual class SystemCollectionViewFactory actual constructor( widget.items.bind(lifecycleOwner) { units -> val list = units.orEmpty() + val onReachEnd = widget.onReachEnd unitsAdapter.units = when { - widget.onReachEnd == null -> list + onReachEnd == null -> list list.isEmpty() -> list else -> list.subList(0, list.lastIndex) + UnitItemDecorator(list.last()) { - widget.onReachEnd.invoke() + onReachEnd.invoke() } } } @@ -94,4 +94,28 @@ actual class SystemCollectionViewFactory actual constructor( margins = margins ) } + + private fun Orientation.toStaggeredGridLayoutManager(): Int = when (this) { + Orientation.VERTICAL -> StaggeredGridLayoutManager.VERTICAL + Orientation.HORIZONTAL -> StaggeredGridLayoutManager.HORIZONTAL + } + + private class UnitItemDecorator( + private val decorated: UnitItem, + val onBind: () -> Unit + ) : UnitItem { + + override val itemId: Long get() = decorated.itemId + + override val viewType: Int get() = decorated.viewType + + override fun bindViewHolder(viewHolder: RecyclerView.ViewHolder) { + decorated.bindViewHolder(viewHolder) + onBind() + } + + override fun createViewHolder(parent: ViewGroup, lifecycleOwner: LifecycleOwner): RecyclerView.ViewHolder { + return decorated.createViewHolder(parent, lifecycleOwner) + } + } } diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/CollectionWidget.kt b/widgets-collection/src/commonMain/kotlin/dev/icerock/moko/widgets/collection/CollectionWidget.kt similarity index 89% rename from widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/CollectionWidget.kt rename to widgets-collection/src/commonMain/kotlin/dev/icerock/moko/widgets/collection/CollectionWidget.kt index 3be42476..7bfff137 100644 --- a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/CollectionWidget.kt +++ b/widgets-collection/src/commonMain/kotlin/dev/icerock/moko/widgets/collection/CollectionWidget.kt @@ -2,7 +2,7 @@ * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.widgets +package dev.icerock.moko.widgets.collection import dev.icerock.moko.mvvm.livedata.LiveData import dev.icerock.moko.units.CollectionUnitItem @@ -13,10 +13,9 @@ import dev.icerock.moko.widgets.core.ViewFactory import dev.icerock.moko.widgets.core.ViewFactoryContext import dev.icerock.moko.widgets.core.Widget import dev.icerock.moko.widgets.core.WidgetDef -import dev.icerock.moko.widgets.factory.SystemCollectionViewFactory import dev.icerock.moko.widgets.style.view.WidgetSize -@WidgetDef(SystemCollectionViewFactory::class) +@WidgetDef(SimpleCollectionViewFactory::class) class CollectionWidget( private val factory: ViewFactory>, override val size: WS, diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/factory/SystemCollectionViewFactory.kt b/widgets-collection/src/commonMain/kotlin/dev/icerock/moko/widgets/collection/SimpleCollectionViewFactory.kt similarity index 74% rename from widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/factory/SystemCollectionViewFactory.kt rename to widgets-collection/src/commonMain/kotlin/dev/icerock/moko/widgets/collection/SimpleCollectionViewFactory.kt index 1f7595ab..a9dc4aef 100644 --- a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/factory/SystemCollectionViewFactory.kt +++ b/widgets-collection/src/commonMain/kotlin/dev/icerock/moko/widgets/collection/SimpleCollectionViewFactory.kt @@ -1,10 +1,9 @@ /* - * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.widgets.factory +package dev.icerock.moko.widgets.collection -import dev.icerock.moko.widgets.CollectionWidget import dev.icerock.moko.widgets.core.ViewFactory import dev.icerock.moko.widgets.style.background.Background import dev.icerock.moko.widgets.style.background.Fill @@ -13,9 +12,8 @@ import dev.icerock.moko.widgets.style.view.MarginValues import dev.icerock.moko.widgets.style.view.PaddingValues import dev.icerock.moko.widgets.style.view.WidgetSize -expect class SystemCollectionViewFactory( +expect class SimpleCollectionViewFactory( orientation: Orientation = Orientation.VERTICAL, - spanCount: Int = 2, padding: PaddingValues? = null, margins: MarginValues? = null, background: Background? = null diff --git a/widgets-collection/src/iosArm64Main b/widgets-collection/src/iosArm64Main new file mode 120000 index 00000000..93d7d747 --- /dev/null +++ b/widgets-collection/src/iosArm64Main @@ -0,0 +1 @@ +iosMain \ No newline at end of file diff --git a/widgets-collection/src/iosMain/kotlin/dev/icerock/moko/widgets/collection/SimpleCollectionViewFactory.kt b/widgets-collection/src/iosMain/kotlin/dev/icerock/moko/widgets/collection/SimpleCollectionViewFactory.kt new file mode 100644 index 00000000..3f1d7042 --- /dev/null +++ b/widgets-collection/src/iosMain/kotlin/dev/icerock/moko/widgets/collection/SimpleCollectionViewFactory.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.collection + +import cocoapods.mokoWidgetsCollection.ALCollectionFlowLayout +import dev.icerock.moko.units.UnitCollectionViewDataSource +import dev.icerock.moko.widgets.core.ViewBundle +import dev.icerock.moko.widgets.core.ViewFactory +import dev.icerock.moko.widgets.core.ViewFactoryContext +import dev.icerock.moko.widgets.style.background.Background +import dev.icerock.moko.widgets.style.background.Fill +import dev.icerock.moko.widgets.style.background.Orientation +import dev.icerock.moko.widgets.style.view.MarginValues +import dev.icerock.moko.widgets.style.view.PaddingValues +import dev.icerock.moko.widgets.style.view.WidgetSize +import dev.icerock.moko.widgets.utils.Edges +import dev.icerock.moko.widgets.utils.applyBackgroundIfNeeded +import dev.icerock.moko.widgets.utils.bind +import dev.icerock.moko.widgets.utils.toEdgeInsets +import kotlinx.cinterop.readValue +import kotlinx.cinterop.useContents +import platform.CoreGraphics.CGRectZero +import platform.UIKit.UICollectionView +import platform.UIKit.UICollectionViewScrollDirection +import platform.UIKit.UIColor +import platform.UIKit.UIEdgeInsetsMake +import platform.UIKit.UIEdgeInsetsZero +import platform.UIKit.backgroundColor +import platform.UIKit.translatesAutoresizingMaskIntoConstraints + +actual class SimpleCollectionViewFactory actual constructor( + private val orientation: Orientation, + private val padding: PaddingValues?, + private val margins: MarginValues?, + private val background: Background? +) : ViewFactory> { + + override fun build( + widget: CollectionWidget, + size: WS, + viewFactoryContext: ViewFactoryContext + ): ViewBundle { + val layoutAndDelegate = ALCollectionFlowLayout().apply { + sectionInset = UIEdgeInsetsZero.readValue() + minimumInteritemSpacing = 0.0 + minimumLineSpacing = 0.0 + scrollDirection = when (orientation) { + Orientation.HORIZONTAL -> UICollectionViewScrollDirection.UICollectionViewScrollDirectionHorizontal + Orientation.VERTICAL -> UICollectionViewScrollDirection.UICollectionViewScrollDirectionVertical + } + } + + val collectionView = UICollectionView( + frame = CGRectZero.readValue(), + collectionViewLayout = layoutAndDelegate + ).apply { + backgroundColor = UIColor.clearColor + + applyBackgroundIfNeeded(background) + + when (orientation) { + Orientation.VERTICAL -> { + this.setAlwaysBounceHorizontal(false) + this.setShowsHorizontalScrollIndicator(false) + } + Orientation.HORIZONTAL -> { + this.setAlwaysBounceVertical(false) + this.setShowsVerticalScrollIndicator(false) + } + } + + padding?.toEdgeInsets()?.also { insetsValue -> + val insets = insetsValue.useContents { + Edges( + top = this.top, + leading = this.left, + trailing = this.right, + bottom = this.bottom + ) + } + contentInset = UIEdgeInsetsMake( + top = insets.top, + bottom = insets.bottom, + left = insets.leading, + right = insets.trailing + ) + } + } + val unitDataSource = UnitCollectionViewDataSource(collectionView) + + with(collectionView) { + translatesAutoresizingMaskIntoConstraints = false + dataSource = unitDataSource + } + + widget.items.bind { unitDataSource.unitItems = it } + + return ViewBundle( + view = collectionView, + size = size, + margins = margins + ) + } + +// @Suppress( +// "CONFLICTING_OVERLOADS", +// "RETURN_TYPE_MISMATCH_ON_INHERITANCE", +// "MANY_INTERFACES_MEMBER_NOT_IMPLEMENTED", +// "DIFFERENT_NAMES_FOR_THE_SAME_PARAMETER_IN_SUPERTYPES" +// ) +// private class SpanCollectionViewLayout( +// private val spanCount: Int, +// private val orientation: Orientation +// ) : UICollectionViewFlowLayout(), UICollectionViewDelegateFlowLayoutProtocol { +// private val reusableStubs = mutableMapOf() +// var dataSource: UnitCollectionViewDataSource? = null +// +// override fun collectionView( +// collectionView: UICollectionView, +// layout: UICollectionViewLayout, +// sizeForItemAtIndexPath: NSIndexPath +// ): CValue { +// +// println("Request cell for indexPath: ${sizeForItemAtIndexPath.debugDescription() ?: "unknown"}") +// +// val fixedItemSize = getFixedCellDimension(collectionView, spanCount, orientation) +// +// val position = sizeForItemAtIndexPath.item.toInt() +// +// val unit = dataSource!!.unitItems!![position] +// +// println("Request stub") +// +// val stub = getStub(collectionView, unit, sizeForItemAtIndexPath) +// objc_sync_enter(stub) +// stub.setTranslatesAutoresizingMaskIntoConstraints(false) +// unit.bind(stub) +// +// with(stub.contentView) { +// translatesAutoresizingMaskIntoConstraints = false +// println("Bind unit to stub") +// +// +// when (orientation) { +// Orientation.VERTICAL -> { +// val constraint = widthAnchor.constraintEqualToConstant(fixedItemSize) +// constraint.setActive(true) +// updateConstraints() +// layoutSubviews() +// val result = CGSizeMake(fixedItemSize, wrapContentHeight(fixedItemSize)) +// removeConstraint(constraint) +// return result +// } +// +// Orientation.HORIZONTAL -> { +// //TODO: Copy logic from vertical +// return CGSizeMake(wrapContentWidth(fixedItemSize), fixedItemSize) +// } +// } +// } +// +// objc_sync_exit(stub) +// } +// +// private fun getStub( +// collectionView: UICollectionView, +// unit: CollectionUnitItem, +// indexPath: NSIndexPath +// ): UICollectionViewCell { +// println("Dequeue cell - ${unit.reusableIdentifier}") +// val exist = reusableStubs[unit.reusableIdentifier] +// return if (exist != null) { +// println("Already stubbed, return") +// exist +// } else { +// println("Dequeue stub") +// +// val stub = collectionView.dequeueReusableCellWithReuseIdentifier( +// unit.reusableIdentifier, +// indexPath +// ) +// reusableStubs[unit.reusableIdentifier] = stub +// stub +// } +// } +// } +} +// +//internal fun getFixedCellDimension( +// forCollectionView: UICollectionView, +// spanCount: Int, +// andOrientation: Orientation +//): CGFloat { +// val collectionViewSize = forCollectionView.bounds.useContents { size } +// +// val sizeExtract: (CGSize) -> CGFloat +// val padExtract: (UIEdgeInsets) -> CGFloat +// +// when (andOrientation) { +// Orientation.VERTICAL -> { +// sizeExtract = { it.width } +// padExtract = { it.left + it.right } +// } +// Orientation.HORIZONTAL -> { +// sizeExtract = { it.height } +// padExtract = { it.top + it.bottom } +// } +// } +// return (sizeExtract(collectionViewSize) +// - forCollectionView.contentInset.useContents(padExtract)) / spanCount +//} \ No newline at end of file diff --git a/widgets-collection/src/iosMain/swift/ALCollectionFlowLayout.swift b/widgets-collection/src/iosMain/swift/ALCollectionFlowLayout.swift new file mode 100644 index 00000000..a6ccdf30 --- /dev/null +++ b/widgets-collection/src/iosMain/swift/ALCollectionFlowLayout.swift @@ -0,0 +1,52 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import UIKit + +@objc public class ALCollectionFlowLayout: UICollectionViewFlowLayout { + func widestCellWidth(bounds: CGRect) -> CGFloat { + guard let collectionView = collectionView else { + return 0 + } + + let insets = collectionView.contentInset + let width = bounds.width - insets.left - insets.right + + if width < 0 { return 0 } + else { return width } + } + + func updateEstimatedItemSize(bounds: CGRect, insets: UIEdgeInsets) { + let width: CGFloat + let height: CGFloat + + if scrollDirection == .horizontal { + width = 200 + height = bounds.height - insets.top - insets.bottom + } else { + width = bounds.width - insets.left - insets.right + height = 200 + } + + estimatedItemSize = CGSize(width: width, height: height) + } + + override public func prepare() { + super.prepare() + + guard let cv = collectionView else { return } + + updateEstimatedItemSize(bounds: cv.bounds, insets: cv.adjustedContentInset) + } + + override public func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + guard let cv = collectionView else { return false } + + let oldSize = cv.bounds.size + guard oldSize != newBounds.size else { return false } + + updateEstimatedItemSize(bounds: newBounds, insets: cv.adjustedContentInset) + return true + } +} diff --git a/widgets-collection/src/iosX64Main b/widgets-collection/src/iosX64Main new file mode 120000 index 00000000..93d7d747 --- /dev/null +++ b/widgets-collection/src/iosX64Main @@ -0,0 +1 @@ +iosMain \ No newline at end of file diff --git a/widgets-datetime-picker/build.gradle.kts b/widgets-datetime-picker/build.gradle.kts new file mode 100644 index 00000000..efbeadad --- /dev/null +++ b/widgets-datetime-picker/build.gradle.kts @@ -0,0 +1,47 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.multiplatform") + id("dev.icerock.mobile.multiplatform") + id("kotlin-android-extensions") + id("maven-publish") +} + +group = "dev.icerock.moko" +version = Versions.Libs.MultiPlatform.mokoWidgets + +android { + compileSdkVersion(Versions.Android.compileSdk) + + defaultConfig { + minSdkVersion(Versions.Android.minSdk) + targetSdkVersion(Versions.Android.targetSdk) + } +} + +dependencies { + mppLibrary(Deps.Libs.MultiPlatform.kotlinStdLib) + mppLibrary(Deps.Libs.MultiPlatform.coroutines) + + mppLibrary(Deps.Libs.MultiPlatform.mokoMvvm) + mppLibrary(Deps.Libs.MultiPlatform.mokoResources) + mppLibrary(Deps.Libs.MultiPlatform.mokoWidgets) + mppLibrary(Deps.Libs.MultiPlatform.klock) + + androidLibrary(Deps.Libs.Android.appCompat) + androidLibrary(Deps.Libs.Android.lifecycle) +} + +publishing { + repositories.maven("https://api.bintray.com/maven/icerockdev/moko/moko-widgets/;publish=1") { + name = "bintray" + + credentials { + username = System.getProperty("BINTRAY_USER") + password = System.getProperty("BINTRAY_KEY") + } + } +} diff --git a/widgets-datetime-picker/src/androidMain/AndroidManifest.xml b/widgets-datetime-picker/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..e30bbf93 --- /dev/null +++ b/widgets-datetime-picker/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/DatePickerDialogHandler.kt b/widgets-datetime-picker/src/androidMain/kotlin/dev/icerock/moko/widgets/datetimepicker/DatePickerDialogHandler.kt similarity index 97% rename from widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/DatePickerDialogHandler.kt rename to widgets-datetime-picker/src/androidMain/kotlin/dev/icerock/moko/widgets/datetimepicker/DatePickerDialogHandler.kt index dfe849b6..996e40b2 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/DatePickerDialogHandler.kt +++ b/widgets-datetime-picker/src/androidMain/kotlin/dev/icerock/moko/widgets/datetimepicker/DatePickerDialogHandler.kt @@ -2,7 +2,7 @@ * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.widgets.screen +package dev.icerock.moko.widgets.datetimepicker import android.app.DatePickerDialog import android.app.Dialog @@ -13,6 +13,7 @@ import androidx.fragment.app.FragmentManager import com.soywiz.klock.DateTime import dev.icerock.moko.graphics.Color import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.widgets.screen.Screen import java.util.* import kotlin.properties.ReadOnlyProperty diff --git a/widgets-datetime-picker/src/androidMain/kotlin/dev/icerock/moko/widgets/datetimepicker/TimePickerDialogExt.kt b/widgets-datetime-picker/src/androidMain/kotlin/dev/icerock/moko/widgets/datetimepicker/TimePickerDialogExt.kt new file mode 100644 index 00000000..6551e0ba --- /dev/null +++ b/widgets-datetime-picker/src/androidMain/kotlin/dev/icerock/moko/widgets/datetimepicker/TimePickerDialogExt.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.datetimepicker + +import android.app.Dialog +import android.app.TimePickerDialog +import android.os.Bundle +import android.os.Parcelable +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import dev.icerock.moko.graphics.Color +import dev.icerock.moko.widgets.screen.Screen +import kotlinx.android.parcel.Parcelize +import kotlin.properties.ReadOnlyProperty + +actual class TimePickerDialogHandler + +actual fun Screen<*>.registerTimePickerDialogHandler( + positive: ((dialogId: Int, hour: Int, minute: Int) -> Unit)?, + negative: ((dialogId: Int) -> Unit)? +): ReadOnlyProperty, TimePickerDialogHandler> = + registerAttachFragmentHook(TimePickerDialogHandler()) { fragment -> + if (fragment !is TimePickerDialogFragment) return@registerAttachFragmentHook + + fragment.positiveAction = positive + fragment.negativeAction = negative + } + +actual fun Screen<*>.showTimePickerDialog( + dialogId: Int, + handler: TimePickerDialogHandler, + factory: TimePickerDialogBuilder.() -> Unit +) { + TimePickerDialogBuilder(dialogId, childFragmentManager).apply { + factory(this) + show() + } +} + +actual class TimePickerDialogBuilder( + private val dialogId: Int, + private val fragmentManager: FragmentManager +) { + private var initialHour = 12 + private var initialMinute = 5 + private var is24HoursFormat = true + + /** + * Don't work for Android, it takes accent color from Theme. + */ + actual fun setAccentColor(color: Color) {} + + actual fun setInitialHour(hour: Int) { + initialHour = validateHours(hour) + } + + actual fun setInitialMinutes(minute: Int) { + initialMinute = validateMinutes(minute) + } + + actual fun is24HoursFormat(flag: Boolean) { + is24HoursFormat = flag + } + + internal fun show() { + TimePickerDialogFragment( + TimePickerDialogFragment.Args(dialogId, initialHour, initialMinute, is24HoursFormat) + ).show(fragmentManager, null) + } +} + +class TimePickerDialogFragment : DialogFragment() { + var positiveAction: ((dialogId: Int, hour: Int, minute: Int) -> Unit)? = null + var negativeAction: ((dialogId: Int) -> Unit)? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val args = arguments?.getParcelable(ARG_KEY) + requireNotNull(args) { "Can't be created without arguments." } + + val dialogId = args.dialogId + val dialog = TimePickerDialog( + context, + TimePickerDialog.OnTimeSetListener { _, hour, minute -> + positiveAction?.invoke(dialogId, hour, minute) + }, + args.initialHour, + args.initialMinute, + args.is24HoursFormat + ) + + dialog.setOnCancelListener { negativeAction?.invoke(dialogId) } + return dialog + } + + @Parcelize + data class Args( + val dialogId: Int, + val initialHour: Int, + val initialMinute: Int, + val is24HoursFormat: Boolean + ) : Parcelable + + companion object { + private const val ARG_KEY = "arg_bundle" + + operator fun invoke(args: Args) = TimePickerDialogFragment().apply { + arguments = Bundle().apply { + putParcelable(ARG_KEY, args) + } + } + + } +} diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/ShowDatePickerExt.kt b/widgets-datetime-picker/src/commonMain/kotlin/dev/icerock/moko/widgets/datetimepicker/ShowDatePickerExt.kt similarity index 89% rename from widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/ShowDatePickerExt.kt rename to widgets-datetime-picker/src/commonMain/kotlin/dev/icerock/moko/widgets/datetimepicker/ShowDatePickerExt.kt index fd4f6fd7..da34a945 100644 --- a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/ShowDatePickerExt.kt +++ b/widgets-datetime-picker/src/commonMain/kotlin/dev/icerock/moko/widgets/datetimepicker/ShowDatePickerExt.kt @@ -2,10 +2,11 @@ * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.widgets.screen +package dev.icerock.moko.widgets.datetimepicker import com.soywiz.klock.DateTime import dev.icerock.moko.graphics.Color +import dev.icerock.moko.widgets.screen.Screen import kotlin.properties.ReadOnlyProperty expect class DatePickerDialogHandler diff --git a/widgets-datetime-picker/src/commonMain/kotlin/dev/icerock/moko/widgets/datetimepicker/TimePickerDialog.kt b/widgets-datetime-picker/src/commonMain/kotlin/dev/icerock/moko/widgets/datetimepicker/TimePickerDialog.kt new file mode 100644 index 00000000..c7fa701f --- /dev/null +++ b/widgets-datetime-picker/src/commonMain/kotlin/dev/icerock/moko/widgets/datetimepicker/TimePickerDialog.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.datetimepicker + +import dev.icerock.moko.graphics.Color +import dev.icerock.moko.widgets.screen.Screen +import kotlin.properties.ReadOnlyProperty + +expect class TimePickerDialogHandler + +expect fun Screen<*>.registerTimePickerDialogHandler( + positive: ((dialogId: Int, hour: Int, minute: Int) -> Unit)?, + negative: ((dialogId: Int) -> Unit)? = null +): ReadOnlyProperty, TimePickerDialogHandler> + +expect class TimePickerDialogBuilder { + fun setAccentColor(color: Color) + fun setInitialHour(hour: Int) + fun setInitialMinutes(minute: Int) + fun is24HoursFormat(flag: Boolean) +} + +expect fun Screen<*>.showTimePickerDialog( + dialogId: Int, + handler: TimePickerDialogHandler, + factory: TimePickerDialogBuilder.() -> Unit +) + +internal fun validateHours(hour: Int) = when { + hour < 0 -> 0 + hour > 23 -> 23 + else -> hour +} + +internal fun validateMinutes(minute: Int) = when { + minute < 0 -> 0 + minute > 59 -> 59 + else -> minute +} diff --git a/widgets-datetime-picker/src/iosArm64Main b/widgets-datetime-picker/src/iosArm64Main new file mode 120000 index 00000000..93d7d747 --- /dev/null +++ b/widgets-datetime-picker/src/iosArm64Main @@ -0,0 +1 @@ +iosMain \ No newline at end of file diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/ShowDatePickerExt.kt b/widgets-datetime-picker/src/iosMain/kotlin/dev/icerock/moko/widgets/datetimepicker/ShowDatePickerExt.kt similarity index 98% rename from widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/ShowDatePickerExt.kt rename to widgets-datetime-picker/src/iosMain/kotlin/dev/icerock/moko/widgets/datetimepicker/ShowDatePickerExt.kt index 9c7b6edc..2ad73e68 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/ShowDatePickerExt.kt +++ b/widgets-datetime-picker/src/iosMain/kotlin/dev/icerock/moko/widgets/datetimepicker/ShowDatePickerExt.kt @@ -1,11 +1,12 @@ /* * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.widgets.screen +package dev.icerock.moko.widgets.datetimepicker import com.soywiz.klock.DateTime import dev.icerock.moko.graphics.Color import dev.icerock.moko.graphics.toUIColor +import dev.icerock.moko.widgets.screen.Screen import dev.icerock.moko.widgets.utils.setEventHandler import kotlin.properties.ReadOnlyProperty import platform.UIKit.UIModalPresentationOverCurrentContext diff --git a/widgets-datetime-picker/src/iosMain/kotlin/dev/icerock/moko/widgets/datetimepicker/TimePickerDialogExt.kt b/widgets-datetime-picker/src/iosMain/kotlin/dev/icerock/moko/widgets/datetimepicker/TimePickerDialogExt.kt new file mode 100644 index 00000000..c9c700ae --- /dev/null +++ b/widgets-datetime-picker/src/iosMain/kotlin/dev/icerock/moko/widgets/datetimepicker/TimePickerDialogExt.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.datetimepicker + +import dev.icerock.moko.graphics.Color +import dev.icerock.moko.graphics.toUIColor +import dev.icerock.moko.widgets.screen.Screen +import dev.icerock.moko.widgets.utils.setEventHandler +import platform.Foundation.NSBundle +import platform.Foundation.NSCalendar +import platform.Foundation.NSCalendarUnitHour +import platform.Foundation.NSCalendarUnitMinute +import platform.Foundation.NSDate +import platform.UIKit.UIApplication +import platform.UIKit.UIButton +import platform.UIKit.UIColor +import platform.UIKit.UIControlEventTouchUpInside +import platform.UIKit.UIControlStateNormal +import platform.UIKit.UIDatePicker +import platform.UIKit.UIDatePickerMode +import platform.UIKit.UIModalPresentationOverCurrentContext +import platform.UIKit.UIView +import platform.UIKit.UIViewController +import platform.UIKit.addSubview +import platform.UIKit.backgroundColor +import platform.UIKit.bottomAnchor +import platform.UIKit.heightAnchor +import platform.UIKit.leadingAnchor +import platform.UIKit.safeAreaLayoutGuide +import platform.UIKit.topAnchor +import platform.UIKit.trailingAnchor +import platform.UIKit.translatesAutoresizingMaskIntoConstraints +import kotlin.properties.ReadOnlyProperty + +actual class TimePickerDialogHandler( + val positive: ((dialogId: Int, hour: Int, minute: Int) -> Unit)?, + val negative: ((dialogId: Int) -> Unit)? +) + +actual fun Screen<*>.registerTimePickerDialogHandler( + positive: ((dialogId: Int, hour: Int, minute: Int) -> Unit)?, + negative: ((dialogId: Int) -> Unit)? +): ReadOnlyProperty, TimePickerDialogHandler> { + return createConstReadOnlyProperty(TimePickerDialogHandler(positive, negative)) +} + +actual fun Screen<*>.showTimePickerDialog( + dialogId: Int, + handler: TimePickerDialogHandler, + factory: TimePickerDialogBuilder.() -> Unit +) { + TimePickerDialogBuilder().apply { + factory(this) + val controller = createViewController(handler, dialogId) + controller.modalPresentationStyle = UIModalPresentationOverCurrentContext + this@showTimePickerDialog.viewController.presentViewController( + controller, + animated = true, + completion = null + ) + } +} + +actual class TimePickerDialogBuilder { + private var initialHour = 12 + private var initialMinute = 5 + private var accentColor: Color? = null + + actual fun setAccentColor(color: Color) { + accentColor = color + } + + actual fun setInitialHour(hour: Int) { + initialHour = validateHours(hour) + } + + actual fun setInitialMinutes(minute: Int) { + initialMinute = validateMinutes(minute) + } + + /** + * Don't work for iOS, it takes format from user local settings in setting.app. + */ + actual fun is24HoursFormat(flag: Boolean) {} + + internal fun createViewController( + handler: TimePickerDialogHandler, + dialogId: Int + ) = TimePickerController(accentColor, initialHour, initialMinute, handler, dialogId) +} + +class TimePickerController( + private val accentColor: Color?, + private val initialHour: Int, + private val initialMinute: Int, + private val handler: TimePickerDialogHandler, + private val dialogId: Int +) : UIViewController(nibName = null, bundle = null) { + + override fun viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.blackColor.colorWithAlphaComponent(0.7) + + val pickerBackground = UIView() + view.addSubview(pickerBackground) + pickerBackground.translatesAutoresizingMaskIntoConstraints = false + pickerBackground.backgroundColor = UIColor.whiteColor + pickerBackground.leadingAnchor.constraintEqualToAnchor( + anchor = view.leadingAnchor + ).active = true + pickerBackground.trailingAnchor.constraintEqualToAnchor( + anchor = view.trailingAnchor + ).active = true + pickerBackground.bottomAnchor.constraintEqualToAnchor( + anchor = view.bottomAnchor + ).active = true + + val datePicker = UIDatePicker() + datePicker.translatesAutoresizingMaskIntoConstraints = false + datePicker.datePickerMode = UIDatePickerMode.UIDatePickerModeTime + view.addSubview(datePicker) + datePicker.leadingAnchor.constraintEqualToAnchor( + anchor = view.leadingAnchor + ).active = true + datePicker.trailingAnchor.constraintEqualToAnchor( + anchor = view.trailingAnchor + ).active = true + datePicker.bottomAnchor.constraintEqualToAnchor( + anchor = view.safeAreaLayoutGuide.bottomAnchor + ).active = true + datePicker.heightAnchor.constraintEqualToConstant(232.0) + + pickerBackground.topAnchor.constraintEqualToAnchor( + anchor = datePicker.topAnchor + ).active = true + + val controlPanel = UIView() + controlPanel.translatesAutoresizingMaskIntoConstraints = false + controlPanel.backgroundColor = UIColor(red = 0.97, green = 0.97, blue = 0.97, alpha = 1.0) + view.addSubview(controlPanel) + controlPanel.leadingAnchor.constraintEqualToAnchor( + anchor = view.leadingAnchor + ).active = true + controlPanel.trailingAnchor.constraintEqualToAnchor( + anchor = view.trailingAnchor + ).active = true + controlPanel.bottomAnchor.constraintEqualToAnchor( + anchor = datePicker.topAnchor + ).active = true + controlPanel.heightAnchor.constraintEqualToConstant(44.0) + + val bundle = NSBundle.bundleForClass(UIApplication) + val doneButton = UIButton() + doneButton.translatesAutoresizingMaskIntoConstraints = false + doneButton.setTitle( + title = bundle.localizedStringForKey( + key = "Done", + value = "Done", + table = null + ) ?: "Done", + forState = UIControlStateNormal + ) + if (accentColor != null) { + doneButton.setTitleColor(accentColor.toUIColor(), UIControlStateNormal) + } + controlPanel.addSubview(doneButton) + doneButton.trailingAnchor.constraintEqualToAnchor( + anchor = controlPanel.trailingAnchor, + constant = -16.0 + ).active = true + doneButton.bottomAnchor.constraintEqualToAnchor( + anchor = controlPanel.bottomAnchor + ).active = true + doneButton.topAnchor.constraintEqualToAnchor( + anchor = controlPanel.topAnchor + ).active = true + + val cancelButton = UIButton() + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.setTitle( + title = bundle.localizedStringForKey( + key = "Cancel", + value = "Cancel", + table = null + ) ?: "Cancel", + forState = UIControlStateNormal + ) + if (accentColor != null) { + cancelButton.setTitleColor(accentColor.toUIColor(), UIControlStateNormal) + } + controlPanel.addSubview(cancelButton) + cancelButton.leadingAnchor.constraintEqualToAnchor( + anchor = controlPanel.leadingAnchor, + constant = 16.0 + ).active = true + cancelButton.bottomAnchor.constraintEqualToAnchor( + anchor = controlPanel.bottomAnchor + ).active = true + cancelButton.topAnchor.constraintEqualToAnchor( + anchor = controlPanel.topAnchor + ).active = true + + val initialComponents = NSCalendar.currentCalendar.components( + NSCalendarUnitHour or NSCalendarUnitMinute, + NSDate() + ).apply { + setHour(initialHour.toLong()) + setMinute(initialMinute.toLong()) + } + NSCalendar.currentCalendar.dateFromComponents(initialComponents)?.let { + datePicker.setDate(it) + } + + doneButton.setEventHandler(UIControlEventTouchUpInside) { + this.dismissViewControllerAnimated(flag = true, completion = null) + val hour = + NSCalendar.currentCalendar.components(NSCalendarUnitHour, datePicker.date).hour + val minute = + NSCalendar.currentCalendar.components(NSCalendarUnitMinute, datePicker.date).minute + handler.positive?.invoke(dialogId, hour.toInt(), minute.toInt()) + } + cancelButton.setEventHandler(UIControlEventTouchUpInside) { + this.dismissViewControllerAnimated(flag = true, completion = null) + handler.negative?.invoke(dialogId) + } + } + +} diff --git a/widgets-datetime-picker/src/iosX64Main b/widgets-datetime-picker/src/iosX64Main new file mode 120000 index 00000000..93d7d747 --- /dev/null +++ b/widgets-datetime-picker/src/iosX64Main @@ -0,0 +1 @@ +iosMain \ No newline at end of file diff --git a/widgets/build.gradle.kts b/widgets/build.gradle.kts index b7e13595..7fbea83f 100644 --- a/widgets/build.gradle.kts +++ b/widgets/build.gradle.kts @@ -36,7 +36,6 @@ dependencies { mppLibrary(Deps.Libs.MultiPlatform.mokoMedia) mppLibrary(Deps.Libs.MultiPlatform.mokoGraphics) mppLibrary(Deps.Libs.MultiPlatform.mokoParcelize) - mppLibrary(Deps.Libs.MultiPlatform.klock) androidLibrary(Deps.Libs.Android.appCompat) androidLibrary(Deps.Libs.Android.fragment) diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/ButtonWithIconViewFactory.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/ButtonWithIconViewFactory.kt index 6db144c4..874535c4 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/ButtonWithIconViewFactory.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/ButtonWithIconViewFactory.kt @@ -86,6 +86,7 @@ actual class ButtonWithIconViewFactory actual constructor( button.applyStateBackgroundIfNeeded(background) button.supportBackgroundTintList = null button.supportBackgroundTintMode = null + button.iconTint = null button.applyPaddingIfNeeded(padding) if (androidElevationEnabled == false && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/FloatingLabelInputViewFactory.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/FloatingLabelInputViewFactory.kt index 36d37550..4c62c6bc 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/FloatingLabelInputViewFactory.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/FloatingLabelInputViewFactory.kt @@ -139,6 +139,7 @@ actual class FloatingLabelInputViewFactory actual constructor( collapsingTextHelper.collapsedTypeface = when (it.fontStyle) { FontStyle.BOLD -> Typeface.DEFAULT_BOLD FontStyle.MEDIUM -> Typeface.DEFAULT + FontStyle.ITALIC -> Typeface.create(Typeface.DEFAULT, Typeface.ITALIC) } collapsingTextHelper.expandedTypeface = collapsingTextHelper.collapsedTypeface } diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemImageViewFactory.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemImageViewFactory.kt index f7a55bb4..f2a7a14d 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemImageViewFactory.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemImageViewFactory.kt @@ -11,6 +11,7 @@ import dev.icerock.moko.widgets.core.ViewBundle import dev.icerock.moko.widgets.core.ViewFactory import dev.icerock.moko.widgets.core.ViewFactoryContext import dev.icerock.moko.widgets.style.view.MarginValues +import dev.icerock.moko.widgets.style.view.SizeSpec import dev.icerock.moko.widgets.style.view.WidgetSize import dev.icerock.moko.widgets.utils.bind import dev.icerock.moko.widgets.utils.dp @@ -35,6 +36,13 @@ actual class SystemImageViewFactory actual constructor( ImageWidget.ScaleType.FILL -> imageView.scaleType = ImageView.ScaleType.CENTER_CROP ImageWidget.ScaleType.FIT -> imageView.scaleType = ImageView.ScaleType.CENTER_INSIDE } + if ((size is WidgetSize.Const<*, *> && + (size.width is SizeSpec.WrapContent || size.height is SizeSpec.WrapContent)) || + (size is WidgetSize.AspectByWidth<*> && size.width is SizeSpec.WrapContent) || + (size is WidgetSize.AspectByHeight<*> && size.height is SizeSpec.WrapContent) + ) { + imageView.adjustViewBounds = true + } widget.image.bind(lifecycleOwner) { image -> if (image == null) { diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemTextViewFactory.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemTextViewFactory.kt index 0ee1c487..a5f01663 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemTextViewFactory.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/factory/SystemTextViewFactory.kt @@ -6,9 +6,9 @@ package dev.icerock.moko.widgets.factory import android.annotation.SuppressLint import android.text.Html +import android.text.TextUtils import android.text.method.LinkMovementMethod import android.view.Gravity -import android.widget.TextView import androidx.appcompat.widget.AppCompatTextView import dev.icerock.moko.graphics.Color import dev.icerock.moko.widgets.TextWidget @@ -65,7 +65,13 @@ actual class SystemTextViewFactory actual constructor( } } } - widget.text.bind(lifecycleOwner) { textView.text = it?.toString(context)?.run(strProcessing) } + widget.maxLines?.bind(lifecycleOwner) { + textView.maxLines = (it ?: Int.MAX_VALUE) + textView.ellipsize = TextUtils.TruncateAt.END + } + widget.text.bind(lifecycleOwner) { + textView.text = it?.toString(context)?.run(strProcessing) + } if (isHtmlConverted) { textView.movementMethod = LinkMovementMethod.getInstance() diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt index 15cd758c..d8bce85b 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt @@ -51,6 +51,10 @@ actual abstract class Screen : Fragment() { return EventsDispatcher(mainExecutor) } + actual open fun onViewCreated() { + + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -59,6 +63,7 @@ actual abstract class Screen : Fragment() { resultCode = getIntNullable(RESULT_CODE_KEY) screenId = getIntNullable(SCREEN_ID_KEY) } + onViewCreated() } override fun onResume() { diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarNormal.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarNormal.kt index af6c340f..e3a0ba0d 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarNormal.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarNormal.kt @@ -46,6 +46,7 @@ fun NavigationBar.Normal.apply( val style = when (fontStyle) { FontStyle.BOLD -> Typeface.BOLD FontStyle.MEDIUM -> Typeface.NORMAL + FontStyle.ITALIC -> Typeface.ITALIC } val styleSpan = StyleSpan(style) setSpan(styleSpan, 0, title.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarSearch.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarSearch.kt index 0a53f4f1..d4c59c2c 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarSearch.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/NavigationBarSearch.kt @@ -48,6 +48,7 @@ fun NavigationBar.Search.apply( val style = when (fontStyle) { FontStyle.BOLD -> Typeface.BOLD FontStyle.MEDIUM -> Typeface.NORMAL + FontStyle.ITALIC -> Typeface.ITALIC } val styleSpan = StyleSpan(style) setSpan(styleSpan, 0, title.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/ext/OrientationExt.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/ext/OrientationExt.kt index 16ff46bb..18825d3f 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/ext/OrientationExt.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/ext/OrientationExt.kt @@ -31,8 +31,3 @@ internal fun Orientation.toRecyclerView(): Int = when (this) { Orientation.VERTICAL -> RecyclerView.VERTICAL Orientation.HORIZONTAL -> RecyclerView.HORIZONTAL } - -internal fun Orientation.toStaggeredGridLayoutManager(): Int = when (this) { - Orientation.VERTICAL -> StaggeredGridLayoutManager.VERTICAL - Orientation.HORIZONTAL -> StaggeredGridLayoutManager.HORIZONTAL -} diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/fontStyle.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/fontStyle.kt index 5567a232..5dd87c23 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/fontStyle.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/style/fontStyle.kt @@ -12,5 +12,6 @@ fun TextView.applyFontStyle(fontStyle: FontStyle) { when (fontStyle) { FontStyle.BOLD -> setTypeface(typeface, Typeface.BOLD) FontStyle.MEDIUM -> setTypeface(typeface, Typeface.NORMAL) + FontStyle.ITALIC -> setTypeface(typeface, Typeface.ITALIC) } } diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt index efb94a6d..37d603b6 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt @@ -4,4 +4,127 @@ package dev.icerock.moko.widgets.units -actual typealias WidgetsCollectionUnitItem = WidgetsTableUnitItem +import android.content.Context +import android.util.DisplayMetrics +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.RecyclerView +import dev.icerock.moko.mvvm.livedata.LiveData +import dev.icerock.moko.mvvm.livedata.MutableLiveData +import dev.icerock.moko.units.UnitItem +import dev.icerock.moko.widgets.core.ViewFactoryContext +import dev.icerock.moko.widgets.core.Widget +import dev.icerock.moko.widgets.style.ext.applyMargin +import dev.icerock.moko.widgets.style.ext.toPlatformSize +import dev.icerock.moko.widgets.style.view.SizeSpec +import dev.icerock.moko.widgets.style.view.WidgetSize +import dev.icerock.moko.widgets.view.AspectRatioFrameLayout + +actual abstract class WidgetsCollectionUnitItem actual constructor( + override val itemId: Long, + val data: T +) : UnitItem { + actual abstract val reuseId: String + actual abstract fun createWidget(data: LiveData): Widget + + override val viewType: Int by lazy { reuseId.hashCode() } + + override fun createViewHolder( + parent: ViewGroup, + lifecycleOwner: LifecycleOwner + ): RecyclerView.ViewHolder { + val mutableData: MutableLiveData = MutableLiveData(initialValue = data) + val widget: Widget = createWidget(mutableData) + val context: Context = parent.context + val view: View = createView(widget, context, lifecycleOwner, parent) + return ViewHolder(mutableData, view) + } + + override fun bindViewHolder(viewHolder: RecyclerView.ViewHolder) { + val widgetViewHolder = viewHolder as ViewHolder + widgetViewHolder.mutableData.value = data + } + + private fun createView( + widget: Widget, + context: Context, + lifecycleOwner: LifecycleOwner, + parent: ViewGroup + ): View { + val viewBundle = widget.buildView( + ViewFactoryContext( + context = context, + lifecycleOwner = lifecycleOwner, + parent = parent + ) + ) + + val dm = context.resources.displayMetrics + val (lp, view) = (viewBundle.size to viewBundle.view) + .toRecyclerViewLayoutParams(dm) + + viewBundle.margins?.let { lp.applyMargin(dm, it) } + + return view.apply { + layoutParams = lp + } + } + + fun Pair.toRecyclerViewLayoutParams( + dm: DisplayMetrics + ): Pair { + val widgetSize = this.first + val view = this.second + return when (widgetSize) { + is WidgetSize.Const -> { + RecyclerView.LayoutParams( + widgetSize.width.toPlatformSize(dm), + widgetSize.height.toPlatformSize(dm) + ) to view + } + is WidgetSize.AspectByWidth -> { + val lp = RecyclerView.LayoutParams( + widgetSize.width.toPlatformSize(dm), + ViewGroup.LayoutParams.WRAP_CONTENT + ) + + lp to AspectRatioFrameLayout( + context = view.context, + aspectRatio = widgetSize.aspectRatio, + aspectByWidth = true + ).apply { + addView( + view, + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + } + is WidgetSize.AspectByHeight -> { + val lp = RecyclerView.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + widgetSize.height.toPlatformSize(dm) + ) + + lp to AspectRatioFrameLayout( + context = view.context, + aspectRatio = widgetSize.aspectRatio, + aspectByWidth = false + ).apply { + addView( + view, + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + } + } + } + + private class ViewHolder( + val mutableData: MutableLiveData, + view: View + ) : RecyclerView.ViewHolder(view) +} + diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/units/WidgetsTableUnitItem.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/units/WidgetsTableUnitItem.kt index 7c20cd8b..a0c70fa8 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/units/WidgetsTableUnitItem.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/units/WidgetsTableUnitItem.kt @@ -4,126 +4,40 @@ package dev.icerock.moko.widgets.units -import android.content.Context -import android.util.DisplayMetrics -import android.view.View import android.view.ViewGroup import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import dev.icerock.moko.mvvm.livedata.LiveData -import dev.icerock.moko.mvvm.livedata.MutableLiveData -import dev.icerock.moko.units.UnitItem -import dev.icerock.moko.widgets.core.ViewFactoryContext +import dev.icerock.moko.units.TableUnitItem import dev.icerock.moko.widgets.core.Widget -import dev.icerock.moko.widgets.style.ext.applyMargin -import dev.icerock.moko.widgets.style.ext.toPlatformSize -import dev.icerock.moko.widgets.style.view.SizeSpec import dev.icerock.moko.widgets.style.view.WidgetSize -import dev.icerock.moko.widgets.view.AspectRatioFrameLayout actual abstract class WidgetsTableUnitItem actual constructor( - override val itemId: Long, - val data: T -) : UnitItem { + itemId: Long, + data: T +) : TableUnitItem { + private val wrapped = object : WidgetsCollectionUnitItem( + itemId = itemId, + data = data + ) { + override val reuseId: String get() = this@WidgetsTableUnitItem.reuseId + + override fun createWidget(data: LiveData): Widget { + return this@WidgetsTableUnitItem.createWidget(data).widget + } + } + actual abstract val reuseId: String actual abstract fun createWidget(data: LiveData): UnitItemRoot - override val viewType: Int by lazy { reuseId.hashCode() } - - override fun createViewHolder( - parent: ViewGroup, - lifecycleOwner: LifecycleOwner - ): RecyclerView.ViewHolder { - val mutableData: MutableLiveData = MutableLiveData(initialValue = data) - val widget: Widget = createWidget(mutableData).widget - val context: Context = parent.context - val view: View = createView(widget, context, lifecycleOwner, parent) - return ViewHolder(mutableData, view) - } + override val itemId: Long get() = wrapped.itemId + override val viewType: Int get() = wrapped.viewType override fun bindViewHolder(viewHolder: RecyclerView.ViewHolder) { - val widgetViewHolder = viewHolder as ViewHolder - widgetViewHolder.mutableData.value = data + wrapped.bindViewHolder(viewHolder) } - private fun createView( - widget: Widget, - context: Context, - lifecycleOwner: LifecycleOwner, - parent: ViewGroup - ): View { - val viewBundle = widget.buildView( - ViewFactoryContext( - context = context, - lifecycleOwner = lifecycleOwner, - parent = parent - ) - ) - - val dm = context.resources.displayMetrics - val (lp, view) = (viewBundle.size to viewBundle.view) - .toRecyclerViewLayoutParams(dm) - - viewBundle.margins?.let { lp.applyMargin(dm, it) } - - return view.apply { - layoutParams = lp - } + override fun createViewHolder(parent: ViewGroup, lifecycleOwner: LifecycleOwner): RecyclerView.ViewHolder { + return wrapped.createViewHolder(parent, lifecycleOwner) } - - fun Pair.toRecyclerViewLayoutParams( - dm: DisplayMetrics - ): Pair { - val widgetSize = this.first - val view = this.second - return when (widgetSize) { - is WidgetSize.Const -> { - RecyclerView.LayoutParams( - widgetSize.width.toPlatformSize(dm), - widgetSize.height.toPlatformSize(dm) - ) to view - } - is WidgetSize.AspectByWidth -> { - val lp = RecyclerView.LayoutParams( - widgetSize.width.toPlatformSize(dm), - ViewGroup.LayoutParams.WRAP_CONTENT - ) - - lp to AspectRatioFrameLayout( - context = view.context, - aspectRatio = widgetSize.aspectRatio, - aspectByWidth = true - ).apply { - addView( - view, - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - } - } - is WidgetSize.AspectByHeight -> { - val lp = RecyclerView.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - widgetSize.height.toPlatformSize(dm) - ) - - lp to AspectRatioFrameLayout( - context = view.context, - aspectRatio = widgetSize.aspectRatio, - aspectByWidth = false - ).apply { - addView( - view, - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - } - } - } - } - - private class ViewHolder( - val mutableData: MutableLiveData, - view: View - ) : RecyclerView.ViewHolder(view) } diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/utils/IdExt.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/utils/IdExt.kt index feebf60e..dde04a2b 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/utils/IdExt.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/utils/IdExt.kt @@ -4,13 +4,50 @@ package dev.icerock.moko.widgets.utils -import androidx.core.view.ViewCompat +import dev.icerock.moko.widgets.BuildConfig import dev.icerock.moko.widgets.core.Theme import dev.icerock.moko.widgets.core.Widget import dev.icerock.moko.widgets.style.view.WidgetSize +import kotlin.math.abs + +private val classIdMap: MutableMap, Int> = mutableMapOf() val > Theme.Id.androidId: Int get() { - // #61 here should be constant and unique id for every Id object. Or android SaveInstanceState will not work :( - return ViewCompat.generateViewId() // this::class.java.hashCode() + val cachedId = classIdMap[this] + if(cachedId != null) return cachedId + + val idString = this.javaClass.name + val hashCode = abs(idString.hashCode()) + val id = hashCode % 0x9000 + // 0x7F - application resources package + // 0x08 - id resource + // 0x1000 - ids is libraries reserved + val fullId = 0x7f081000 + id + + if (BuildConfig.DEBUG) { + println(String.format("id %s transformed to 0x%X", idString, fullId)) + } + + if (classIdMap.containsValue(fullId)) { + val conflictName: String = classIdMap + .filter { it.value == fullId } + .keys.toList() + .first() + .javaClass.name + + val msg = String.format( + "id 0x%X already used by %s, it conflict with %s", + fullId, + conflictName, + idString + ) + throw AndroidIdConflictException(msg) + } + + classIdMap[this] = fullId + + return fullId } + +private class AndroidIdConflictException(message: String) : RuntimeException(message) diff --git a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/view/UnitItemDecorator.kt b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/view/UnitItemDecorator.kt index eb273ca7..d7989671 100644 --- a/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/view/UnitItemDecorator.kt +++ b/widgets/src/androidMain/kotlin/dev/icerock/moko/widgets/view/UnitItemDecorator.kt @@ -14,10 +14,6 @@ internal class UnitItemDecorator( val onBind: () -> Unit ) : UnitItem { -// init { -// layoutParams = decorated.layoutParams -// } - override val itemId: Long get() = decorated.itemId override val viewType: Int get() = decorated.viewType diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/TextWidget.kt b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/TextWidget.kt index 45c9ff71..caa29c70 100644 --- a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/TextWidget.kt +++ b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/TextWidget.kt @@ -21,7 +21,8 @@ class TextWidget( private val factory: ViewFactory>, override val size: WS, override val id: Id?, - val text: LiveData + val text: LiveData, + val maxLines: LiveData? = null ) : Widget(), OptionalId { override fun buildView(viewFactoryContext: ViewFactoryContext): ViewBundle { diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt index d7c79d6b..ccc6b919 100644 --- a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt +++ b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt @@ -14,6 +14,8 @@ expect abstract class Screen() { fun createEventsDispatcher(): EventsDispatcher + open fun onViewCreated() + open val androidStatusBarColor: Color? open val isLightStatusBar: Boolean? } diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/TemplateScreen.kt b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/TemplateScreen.kt new file mode 100644 index 00000000..1c783909 --- /dev/null +++ b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/screen/TemplateScreen.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.widgets.screen + +import dev.icerock.moko.resources.desc.StringDesc +import dev.icerock.moko.resources.desc.desc +import dev.icerock.moko.widgets.constraint +import dev.icerock.moko.widgets.core.Theme +import dev.icerock.moko.widgets.core.Widget +import dev.icerock.moko.widgets.screen.navigation.NavigationBar +import dev.icerock.moko.widgets.screen.navigation.NavigationItem +import dev.icerock.moko.widgets.style.view.SizeSpec +import dev.icerock.moko.widgets.style.view.WidgetSize +import dev.icerock.moko.widgets.text + +class TemplateScreen ( + private val navTitle: StringDesc, + private val labelText: StringDesc, + private val theme: Theme +) : WidgetScreen(), NavigationItem { + override val navigationBar = NavigationBar.Normal(title = navTitle) + + override fun createContentWidget() : Widget> = with(theme) { + constraint(size = WidgetSize.AsParent) { + val label = +text( + size = WidgetSize.WrapContent, + text = const(labelText) + ) + + constraints { + label centerXToCenterX root + label centerYToCenterY root + } + } + } +} diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/style/view/FontStyle.kt b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/style/view/FontStyle.kt index 078de8c2..7cbb33b7 100644 --- a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/style/view/FontStyle.kt +++ b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/style/view/FontStyle.kt @@ -5,5 +5,5 @@ package dev.icerock.moko.widgets.style.view enum class FontStyle { - BOLD, MEDIUM + BOLD, MEDIUM, ITALIC } diff --git a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt index 923342a5..cb9b5ccd 100644 --- a/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt +++ b/widgets/src/commonMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt @@ -6,6 +6,8 @@ package dev.icerock.moko.widgets.units import dev.icerock.moko.mvvm.livedata.LiveData import dev.icerock.moko.units.CollectionUnitItem +import dev.icerock.moko.widgets.core.Widget +import dev.icerock.moko.widgets.style.view.WidgetSize expect abstract class WidgetsCollectionUnitItem( itemId: Long, @@ -13,5 +15,5 @@ expect abstract class WidgetsCollectionUnitItem( ) : CollectionUnitItem { abstract val reuseId: String - abstract fun createWidget(data: LiveData): UnitItemRoot + abstract fun createWidget(data: LiveData): Widget } diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/ButtonWithIconViewFactory.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/ButtonWithIconViewFactory.kt index db16ef79..7c9d0118 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/ButtonWithIconViewFactory.kt +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/ButtonWithIconViewFactory.kt @@ -26,7 +26,7 @@ import dev.icerock.moko.widgets.utils.setEventHandler import kotlinx.cinterop.useContents import platform.CoreGraphics.CGFloat import platform.UIKit.UIButton -import platform.UIKit.UIButtonTypeSystem +import platform.UIKit.UIButtonTypeCustom import platform.UIKit.UIControlContentHorizontalAlignment import platform.UIKit.UIControlContentHorizontalAlignmentCenter import platform.UIKit.UIControlContentHorizontalAlignmentLeft @@ -59,7 +59,7 @@ actual class ButtonWithIconViewFactory actual constructor( viewFactoryContext: ViewFactoryContext ): ViewBundle { val button = UIButton.buttonWithType( - buttonType = UIButtonTypeSystem + buttonType = UIButtonTypeCustom ).apply { translatesAutoresizingMaskIntoConstraints = false diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/SystemCollectionViewFactory.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/SystemCollectionViewFactory.kt deleted file mode 100644 index c51e1fae..00000000 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/SystemCollectionViewFactory.kt +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.widgets.factory - -import dev.icerock.moko.units.CollectionUnitItem -import dev.icerock.moko.units.UnitCollectionViewDataSource -import dev.icerock.moko.widgets.CollectionWidget -import dev.icerock.moko.widgets.core.ViewBundle -import dev.icerock.moko.widgets.core.ViewFactory -import dev.icerock.moko.widgets.core.ViewFactoryContext -import dev.icerock.moko.widgets.style.background.Background -import dev.icerock.moko.widgets.style.background.Fill -import dev.icerock.moko.widgets.style.background.Orientation -import dev.icerock.moko.widgets.style.view.MarginValues -import dev.icerock.moko.widgets.style.view.PaddingValues -import dev.icerock.moko.widgets.style.view.WidgetSize -import dev.icerock.moko.widgets.utils.Edges -import dev.icerock.moko.widgets.utils.applyBackgroundIfNeeded -import dev.icerock.moko.widgets.utils.bind -import dev.icerock.moko.widgets.utils.toEdgeInsets -import kotlinx.cinterop.CValue -import kotlinx.cinterop.readValue -import kotlinx.cinterop.useContents -import platform.CoreGraphics.CGRectZero -import platform.CoreGraphics.CGSize -import platform.CoreGraphics.CGSizeMake -import platform.Foundation.NSIndexPath -import platform.UIKit.UICollectionView -import platform.UIKit.UICollectionViewCell -import platform.UIKit.UICollectionViewDelegateFlowLayoutProtocol -import platform.UIKit.UICollectionViewFlowLayout -import platform.UIKit.UICollectionViewLayout -import platform.UIKit.UIColor -import platform.UIKit.UIEdgeInsetsMake -import platform.UIKit.UIEdgeInsetsZero -import platform.UIKit.UILayoutPriorityDefaultLow -import platform.UIKit.UILayoutPriorityRequired -import platform.UIKit.backgroundColor -import platform.UIKit.layoutIfNeeded -import platform.UIKit.row -import platform.UIKit.setNeedsLayout -import platform.UIKit.systemLayoutSizeFittingSize -import platform.UIKit.translatesAutoresizingMaskIntoConstraints - -actual class SystemCollectionViewFactory actual constructor( - private val orientation: Orientation, - private val spanCount: Int, - private val padding: PaddingValues?, - private val margins: MarginValues?, - private val background: Background? -) : ViewFactory> { - - override fun build( - widget: CollectionWidget, - size: WS, - viewFactoryContext: ViewFactoryContext - ): ViewBundle { - val layoutAndDelegate = SpanCollectionViewLayout(spanCount).apply { - sectionInset = UIEdgeInsetsZero.readValue() - minimumInteritemSpacing = 0.0 - minimumLineSpacing = 0.0 - } - val collectionView = UICollectionView( - frame = CGRectZero.readValue(), - collectionViewLayout = layoutAndDelegate - ).apply { - backgroundColor = UIColor.clearColor - delegate = layoutAndDelegate - - applyBackgroundIfNeeded(background) - - padding?.toEdgeInsets()?.also { insetsValue -> - val insets = insetsValue.useContents { - Edges( - top = this.top, - leading = this.left, - trailing = this.right, - bottom = this.bottom - ) - } - contentInset = UIEdgeInsetsMake( - top = insets.top, - bottom = insets.bottom, - left = insets.leading, - right = insets.trailing - ) -// val margins = UIEdgeInsetsMake( -// top = 0.0, -// bottom = 0.0, -// left = insets.leading, -// right = insets.trailing -// ) -// layoutMargins = margins - } - - // TODO add orientation support - } - val unitDataSource = UnitCollectionViewDataSource(collectionView) - layoutAndDelegate.dataSource = unitDataSource - - with(collectionView) { - translatesAutoresizingMaskIntoConstraints = false - dataSource = unitDataSource - } - - widget.items.bind { unitDataSource.unitItems = it } - - return ViewBundle( - view = collectionView, - size = size, - margins = margins - ) - } - - @Suppress( - "CONFLICTING_OVERLOADS", - "RETURN_TYPE_MISMATCH_ON_INHERITANCE", - "MANY_INTERFACES_MEMBER_NOT_IMPLEMENTED", - "DIFFERENT_NAMES_FOR_THE_SAME_PARAMETER_IN_SUPERTYPES" - ) - private class SpanCollectionViewLayout( - private val spanCount: Int - ) : UICollectionViewFlowLayout(), UICollectionViewDelegateFlowLayoutProtocol { - private val reusableStubs = mutableMapOf() - var dataSource: UnitCollectionViewDataSource? = null - - override fun collectionView( - collectionView: UICollectionView, - layout: UICollectionViewLayout, - sizeForItemAtIndexPath: NSIndexPath - ): CValue { -// println("size: $sizeForItemAtIndexPath") - val collectionViewSize = collectionView.bounds.useContents { size } - val width = (collectionViewSize.width - collectionView.contentInset.useContents { - this.left + this.right - }) / spanCount - val position = sizeForItemAtIndexPath.row.toInt() - -// println("width: $width") - - val unit = dataSource!!.unitItems!![position] - // TODO create correct cell from unit - val stub = UICollectionViewCell()//getStub(collectionView, unit, sizeForItemAtIndexPath) - - val size = with(stub.contentView) { - translatesAutoresizingMaskIntoConstraints = false - unit.bind(stub) - - setNeedsLayout() - layoutIfNeeded() - - systemLayoutSizeFittingSize( - CGSizeMake(width, 0.0), - UILayoutPriorityRequired, - UILayoutPriorityDefaultLow - ) - } - - val (rw, rh) = size.useContents { width to height } - -// println("sized: $rw $rh") - - return CGSizeMake(rw, rh) - } - - private fun getStub( - collectionView: UICollectionView, - unit: CollectionUnitItem, - indexPath: NSIndexPath - ): UICollectionViewCell { - val exist = reusableStubs[unit.reusableIdentifier] - return if (exist != null) exist - else { - val stub = collectionView.dequeueReusableCellWithReuseIdentifier( - unit.reusableIdentifier, - indexPath - ) - reusableStubs[unit.reusableIdentifier] = stub - stub - } - } - } -} diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/SystemTextViewFactory.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/SystemTextViewFactory.kt index ff45622e..59aa650c 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/SystemTextViewFactory.kt +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/factory/SystemTextViewFactory.kt @@ -98,6 +98,10 @@ actual class SystemTextViewFactory actual constructor( widget.text.bind { label.text = it.localized() } } + widget.maxLines?.bind { + label.numberOfLines = (it ?: 0).toLong() + } + return ViewBundle( view = label, size = size, diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt index 08ddbe98..b22aba17 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/screen/Screen.kt @@ -34,6 +34,10 @@ actual abstract class Screen { return created } + actual open fun onViewCreated() { + + } + actual fun createEventsDispatcher(): EventsDispatcher { return EventsDispatcher() } @@ -49,6 +53,7 @@ actual abstract class Screen { val vc = createViewController(isLightStatusBar).also { setAssociatedObject(it, this) } + onViewCreated() _viewController = WeakReference(vc) return vc } diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/UnitItemExt.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/UnitItemExt.kt index 24235a8c..99d92a33 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/UnitItemExt.kt +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/UnitItemExt.kt @@ -17,9 +17,13 @@ import platform.UIKit.bottomAnchor import platform.UIKit.layoutMargins import platform.UIKit.layoutMarginsGuide import platform.UIKit.leadingAnchor +import platform.UIKit.setTranslatesAutoresizingMaskIntoConstraints import platform.UIKit.topAnchor import platform.UIKit.trailingAnchor import platform.UIKit.translatesAutoresizingMaskIntoConstraints +import dev.icerock.moko.widgets.core.Widget +import dev.icerock.moko.widgets.style.view.WidgetSize +import platform.UIKit.UILayoutPriorityDefaultHigh private fun UIView.getWidgetLiveData(): MutableLiveData? { return getAssociatedObject(this) as? MutableLiveData @@ -31,7 +35,7 @@ private fun UIView.setWidgetLiveData(liveData: MutableLiveData) { internal fun UIView.setupWidgetContent( data: T, - factory: (liveData: LiveData) -> UnitItemRoot + factory: (liveData: LiveData) -> Widget ) { val liveData = this.getWidgetLiveData() if (liveData != null) { @@ -40,7 +44,7 @@ internal fun UIView.setupWidgetContent( // TODO get correct viewcontroller for widgets val viewController = UIApplication.sharedApplication.keyWindow?.rootViewController!! val mutableLiveData = MutableLiveData(initialValue = data) - val widget = factory(mutableLiveData).widget + val widget = factory(mutableLiveData) val viewBundle = widget.buildView(viewFactoryContext = viewController) val view = viewBundle.view.apply { translatesAutoresizingMaskIntoConstraints = false @@ -54,30 +58,38 @@ internal fun UIView.setupWidgetContent( val childMargins = viewBundle.margins val edges = dev.icerock.moko.widgets.utils.Edges( - top = childMargins?.top?.toDouble() ?: 0.0, - leading = childMargins?.start?.toDouble() ?: 0.0 + margin_left, - bottom = childMargins?.bottom?.toDouble() ?: 0.0, - trailing = childMargins?.end?.toDouble() ?: 0.0 + margin_right + top = childMargins?.top?.toDouble() ?: 0.0, //TODO: Support this + leading = childMargins?.start?.toDouble() ?: 0.0, // + margin_left, + bottom = childMargins?.bottom?.toDouble() ?: 0.0, //TODO: Support this + trailing = childMargins?.end?.toDouble() ?: 0.0 //+ margin_right ) - view.applySize(childSize, this, edges) view.topAnchor.constraintEqualToAnchor( anchor = topAnchor, constant = edges.top ).active = true + view.leadingAnchor.constraintEqualToAnchor( anchor = leadingAnchor, constant = edges.leading ).active = true - view.trailingAnchor.constraintEqualToAnchor( - anchor = trailingAnchor, - constant = -edges.trailing - ).active = true + + trailingAnchor.constraintEqualToAnchor( + anchor = view.trailingAnchor, + constant = edges.trailing + ).apply { + active = true + priority = UILayoutPriorityDefaultHigh + } + bottomAnchor.constraintEqualToAnchor( anchor = view.bottomAnchor, constant = edges.bottom - ).active = true + ).apply { + active = true + priority = UILayoutPriorityDefaultHigh + } this.setWidgetLiveData(mutableLiveData) } diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt index b8828e15..14bd8d2c 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/WidgetsCollectionUnitItem.kt @@ -8,13 +8,16 @@ import dev.icerock.moko.mvvm.livedata.LiveData import dev.icerock.moko.units.CollectionUnitItem import platform.UIKit.UICollectionView import platform.UIKit.UICollectionViewCell +import platform.UIKit.translatesAutoresizingMaskIntoConstraints +import dev.icerock.moko.widgets.core.Widget +import dev.icerock.moko.widgets.style.view.WidgetSize actual abstract class WidgetsCollectionUnitItem actual constructor( override val itemId: Long, private val data: T ) : CollectionUnitItem { actual abstract val reuseId: String - actual abstract fun createWidget(data: LiveData): UnitItemRoot + actual abstract fun createWidget(data: LiveData): Widget override val reusableIdentifier: String get() = reuseId @@ -25,7 +28,7 @@ actual abstract class WidgetsCollectionUnitItem actual constructor( ) } - override fun bind(cell: UICollectionViewCell) { - cell.contentView.setupWidgetContent(data, ::createWidget) + override fun bind(collectionViewCell: UICollectionViewCell) { + collectionViewCell.contentView.setupWidgetContent(data, ::createWidget) } } diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/WidgetsTableUnitItem.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/WidgetsTableUnitItem.kt index 9db12b11..86592cde 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/WidgetsTableUnitItem.kt +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/units/WidgetsTableUnitItem.kt @@ -11,6 +11,8 @@ import platform.UIKit.UITableView import platform.UIKit.UITableViewCell import platform.UIKit.UIView import platform.UIKit.subviews +import dev.icerock.moko.widgets.core.Widget +import dev.icerock.moko.widgets.style.view.WidgetSize actual abstract class WidgetsTableUnitItem actual constructor( override val itemId: Long, @@ -29,7 +31,7 @@ actual abstract class WidgetsTableUnitItem actual constructor( } override fun bind(cell: UITableViewCell) { - cell.contentView.setupWidgetContent(data, ::createWidget) + cell.contentView.setupWidgetContent(data) { createWidget(it).widget } findAnimated(cell.contentView).forEach { it.startAnimating() } } diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/utils/TextStyleExt.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/utils/TextStyleExt.kt index cf53dc2c..5af9821b 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/utils/TextStyleExt.kt +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/utils/TextStyleExt.kt @@ -22,17 +22,20 @@ import platform.UIKit.UIFont import platform.UIKit.UIFontWeightMedium import platform.UIKit.UILabel import platform.UIKit.UITextField -import platform.UIKit.systemFontSize import platform.UIKit.UITextView -fun TextStyle<*>.toUIFont(defaultFontSize: Double = 17.0): UIFont? { // If this is ok, can be applied to other methods +fun TextStyle<*>.toUIFont(defaultFontSize: Double = 17.0): UIFont? { val styleSize = size?.toDouble() val fontStyle = fontStyle if (fontStyle != null || styleSize != null) { val fontSize = styleSize ?: defaultFontSize return when (fontStyle) { FontStyle.BOLD -> UIFont.boldSystemFontOfSize(fontSize = fontSize) - FontStyle.MEDIUM -> UIFont.systemFontOfSize(fontSize = fontSize, weight = UIFontWeightMedium) + FontStyle.MEDIUM -> UIFont.systemFontOfSize( + fontSize = fontSize, + weight = UIFontWeightMedium + ) + FontStyle.ITALIC -> UIFont.italicSystemFontOfSize(fontSize = fontSize) else -> UIFont.systemFontOfSize(fontSize = fontSize) } } @@ -42,16 +45,8 @@ fun TextStyle<*>.toUIFont(defaultFontSize: Double = 17.0): UIFont? { // If this fun UIButton.applyTextStyleIfNeeded(textStyle: TextStyle>?) { if (textStyle == null) return - val currentFontSize = titleLabel?.font?.pointSize ?: 14.0 - val styleSize = textStyle.size?.toDouble() - val styleStyle = textStyle.fontStyle - if (styleStyle != null || styleSize != null) { - val fontSize = styleSize ?: currentFontSize - titleLabel?.font = when (styleStyle) { - FontStyle.BOLD -> UIFont.boldSystemFontOfSize(fontSize = fontSize) - else -> UIFont.systemFontOfSize(fontSize = fontSize) - } - } + val textFont = textStyle.toUIFont(14.0) + if (textFont != null) titleLabel?.font = textFont textStyle.color?.also { setTitleColor(color = it.normal.toUIColor(), forState = UIControlStateNormal) @@ -63,32 +58,14 @@ fun UIButton.applyTextStyleIfNeeded(textStyle: TextStyle>? fun UILabel.applyTextStyleIfNeeded(textStyle: TextStyle?) { if (textStyle == null) return - val currentFontSize = font.pointSize - val styleSize = textStyle.size?.toDouble() - val fontStyle = textStyle.fontStyle - if (fontStyle != null || styleSize != null) { - val fontSize = styleSize ?: currentFontSize - font = when (fontStyle) { - FontStyle.BOLD -> UIFont.boldSystemFontOfSize(fontSize = fontSize) - else -> UIFont.systemFontOfSize(fontSize = fontSize) - } - } + font = textStyle.toUIFont() ?: font textStyle.color?.also { textColor = it.toUIColor() } } fun UITextField.applyTextStyleIfNeeded(textStyle: TextStyle?) { if (textStyle == null) return - val currentFontSize = font?.pointSize ?: UIFont.systemFontSize - val styleSize = textStyle.size?.toDouble() - val fontStyle = textStyle.fontStyle - if (fontStyle != null || styleSize != null) { - val fontSize = styleSize ?: currentFontSize - font = when (fontStyle) { - FontStyle.BOLD -> UIFont.boldSystemFontOfSize(fontSize = fontSize) - else -> UIFont.systemFontOfSize(fontSize = fontSize) - } - } + font = textStyle.toUIFont() ?: font textStyle.color?.also { textColor = it.toUIColor() } } @@ -122,15 +99,6 @@ fun CATextLayer.applyTextStyleIfNeeded(textStyle: TextStyle?) { fun UITextView.applyTextStyleIfNeeded(textStyle: TextStyle?) { if (textStyle == null) return - val currentFontSize = font?.pointSize ?: UIFont.systemFontSize - val styleSize = textStyle.size?.toDouble() - val fontStyle = textStyle.fontStyle - if (fontStyle != null || styleSize != null) { - val fontSize = styleSize ?: currentFontSize - font = when (fontStyle) { - FontStyle.BOLD -> UIFont.boldSystemFontOfSize(fontSize = fontSize) - else -> UIFont.systemFontOfSize(fontSize = fontSize) - } - } + font = textStyle.toUIFont() ?: font textStyle.color?.also { textColor = it.toUIColor() } } diff --git a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/utils/UIViewExt.kt b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/utils/UIViewExt.kt index c1476657..8f46dbbc 100644 --- a/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/utils/UIViewExt.kt +++ b/widgets/src/iosMain/kotlin/dev/icerock/moko/widgets/utils/UIViewExt.kt @@ -12,7 +12,9 @@ import kotlinx.cinterop.CValue import kotlinx.cinterop.readValue import kotlinx.cinterop.useContents import platform.CoreGraphics.CGFloat +import platform.CoreGraphics.CGSize import platform.CoreGraphics.CGRectMake +import platform.CoreGraphics.CGSizeMake import platform.UIKit.NSLayoutDimension import platform.UIKit.UIEdgeInsets import platform.UIKit.UIEdgeInsetsMake @@ -165,21 +167,39 @@ fun PaddingValues.toEdgeInsets(): CValue { } fun UIView.wrapContentHeight(width: CGFloat? = null): CGFloat { - val oldFrame = frame() + var fittingSize = CGSizeMake(width ?: 2000.0, UILayoutFittingCompressedSize.height) + + println("Trying to calculate height by content width: ${fittingSize.useContents { width }}") + + updateConstraints() + layoutSubviews() + + val resultSize = systemLayoutSizeFittingSize( + fittingSize, + withHorizontalFittingPriority = UILayoutPriorityRequired, + verticalFittingPriority = UILayoutPriorityDefaultLow + ).useContents{ this } + println("Calculate wrap content size: W: ${resultSize.width} H: ${resultSize.height}") + return resultSize.height +} + +fun UIView.wrapContentWidth(height: CGFloat? = null): CGFloat { val expandedFrame = CGRectMake( 0.0, 0.0, - width ?: UIScreen.mainScreen.bounds.useContents { this.size.width }, - UIScreen.mainScreen.bounds.useContents { this.size.height } + UIScreen.mainScreen.bounds.useContents { size.width }, + height ?: UIScreen.mainScreen.bounds.useContents { size.height } ) - setFrame(expandedFrame) + println("== Trying to calculate height with from initial size: W: ${expandedFrame.useContents { size }.width} H: ${expandedFrame.useContents { size }.height} ==") + height?.let { + heightAnchor.constraintEqualToConstant(it).setActive(true) + } updateConstraints() layoutSubviews() - val result = systemLayoutSizeFittingSize( - UILayoutFittingCompressedSize.readValue(), - withHorizontalFittingPriority = UILayoutPriorityRequired, - verticalFittingPriority = UILayoutPriorityDefaultLow - ).useContents { this.height } - setFrame(oldFrame) - return result + val resultSize = systemLayoutSizeFittingSize( + expandedFrame.useContents { size }.readValue(), + withHorizontalFittingPriority = UILayoutPriorityDefaultLow, + verticalFittingPriority = UILayoutPriorityRequired).useContents { this } + println("== Calculate wrap content size: W: ${resultSize.width} H: ${resultSize.height} ==") + return resultSize.width } \ No newline at end of file