From e40672bf07921936daabe301df0355cceffe526b Mon Sep 17 00:00:00 2001 From: zhuwenbo Date: Fri, 13 Jan 2023 16:58:53 +0800 Subject: [PATCH 001/110] Fix rememberDrawablePainter for BitmapDrawable --- .../com/google/accompanist/drawablepainter/DrawablePainter.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt b/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt index 319b9a6b3..590674cef 100644 --- a/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt +++ b/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt @@ -17,7 +17,6 @@ package com.google.accompanist.drawablepainter import android.graphics.drawable.Animatable -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.os.Build @@ -34,11 +33,9 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.asAndroidColorFilter -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.withSave @@ -155,7 +152,6 @@ class DrawablePainter( fun rememberDrawablePainter(drawable: Drawable?): Painter = remember(drawable) { when (drawable) { null -> EmptyPainter - is BitmapDrawable -> BitmapPainter(drawable.bitmap.asImageBitmap()) is ColorDrawable -> ColorPainter(Color(drawable.color)) // Since the DrawablePainter will be remembered and it implements RememberObserver, it // will receive the necessary events From 524143507e7215202c66469dc4ed5939d9e794be Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Tue, 15 Nov 2022 15:30:35 +1100 Subject: [PATCH 002/110] Refactor webview to not need to override url loading --- gradle/libs.versions.toml | 4 +- sample/build.gradle | 7 + .../sample/webview/BasicWebViewSample.kt | 17 +- web/api/current.api | 21 +- web/src/androidTest/assets/test_link.html | 25 +++ .../com/google/accompanist/web/WebTest.kt | 57 +++--- .../com/google/accompanist/web/WebView.kt | 186 +++++++++++------- 7 files changed, 201 insertions(+), 116 deletions(-) create mode 100644 web/src/androidTest/assets/test_link.html diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81871ba3b..dbfb3c880 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -83,8 +83,8 @@ androidx-test-orchestrator = "androidx.test:orchestrator:1.4.1" androidx-test-uiAutomator = "androidx.test.uiautomator:uiautomator:2.2.0" # alpha for robolectric x compose fix -androidx-test-espressoCore = "androidx.test.espresso:espresso-core:3.5.0-alpha07" -androidx-test-espressoWeb = "androidx.test.espresso:espresso-web:3.5.0-alpha07" +androidx-test-espressoCore = "androidx.test.espresso:espresso-core:3.5.1" +androidx-test-espressoWeb = "androidx.test.espresso:espresso-web:3.5.1" junit = "junit:junit:4.13.2" truth = "com.google.truth:truth:1.1.2" diff --git a/sample/build.gradle b/sample/build.gradle index 90d12e5e8..acbd8f0db 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -44,6 +44,13 @@ android { composeOptions { kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } + namespace 'com.google.accompanist.sample' } diff --git a/sample/src/main/java/com/google/accompanist/sample/webview/BasicWebViewSample.kt b/sample/src/main/java/com/google/accompanist/sample/webview/BasicWebViewSample.kt index 878dbb8b9..ad2b18055 100644 --- a/sample/src/main/java/com/google/accompanist/sample/webview/BasicWebViewSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/webview/BasicWebViewSample.kt @@ -59,16 +59,16 @@ import com.google.accompanist.web.rememberWebViewNavigator import com.google.accompanist.web.rememberWebViewState class BasicWebViewSample : ComponentActivity() { + val initialUrl = "https://google.com" @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AccompanistSampleTheme { - var url by remember { mutableStateOf("https://google.com") } - val state = rememberWebViewState(url = url) + val state = rememberWebViewState(url = initialUrl) val navigator = rememberWebViewNavigator() - var textFieldValue by remember(state.content.getCurrentUrl()) { - mutableStateOf(state.content.getCurrentUrl() ?: "") + var textFieldValue by remember(state.lastLoadedUrl) { + mutableStateOf(state.lastLoadedUrl) } Column { @@ -100,7 +100,7 @@ class BasicWebViewSample : ComponentActivity() { } OutlinedTextField( - value = textFieldValue, + value = textFieldValue ?: "", onValueChange = { textFieldValue = it }, modifier = Modifier.fillMaxWidth() ) @@ -108,7 +108,9 @@ class BasicWebViewSample : ComponentActivity() { Button( onClick = { - url = textFieldValue + textFieldValue?.let { + navigator.loadUrl(it) + } }, modifier = Modifier.align(Alignment.CenterVertically) ) { @@ -140,7 +142,8 @@ class BasicWebViewSample : ComponentActivity() { WebView( state = state, - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f), navigator = navigator, onCreated = { webView -> webView.settings.javaScriptEnabled = true diff --git a/web/api/current.api b/web/api/current.api index bd8c9f116..baceb0d40 100644 --- a/web/api/current.api +++ b/web/api/current.api @@ -38,18 +38,27 @@ package com.google.accompanist.web { } public abstract sealed class WebContent { - method public final String? getCurrentUrl(); + method @Deprecated public final String? getCurrentUrl(); } public static final class WebContent.Data extends com.google.accompanist.web.WebContent { - ctor public WebContent.Data(String data, optional String? baseUrl); + ctor public WebContent.Data(String data, optional String? baseUrl, optional String encoding, optional String? mimeType, optional String? historyUrl); method public String component1(); method public String? component2(); - method public com.google.accompanist.web.WebContent.Data copy(String data, String? baseUrl); + method public String component3(); + method public String? component4(); + method public String? component5(); + method public com.google.accompanist.web.WebContent.Data copy(String data, String? baseUrl, String encoding, String? mimeType, String? historyUrl); method public String? getBaseUrl(); method public String getData(); + method public String getEncoding(); + method public String? getHistoryUrl(); + method public String? getMimeType(); property public final String? baseUrl; property public final String data; + property public final String encoding; + property public final String? historyUrl; + property public final String? mimeType; } public static final class WebContent.Url extends com.google.accompanist.web.WebContent { @@ -78,13 +87,15 @@ package com.google.accompanist.web { method @androidx.compose.runtime.Composable public static void WebView(com.google.accompanist.web.WebViewState state, optional androidx.compose.ui.Modifier modifier, optional boolean captureBackPresses, optional com.google.accompanist.web.WebViewNavigator navigator, optional kotlin.jvm.functions.Function1 onCreated, optional kotlin.jvm.functions.Function1 onDispose, optional com.google.accompanist.web.AccompanistWebViewClient client, optional com.google.accompanist.web.AccompanistWebChromeClient chromeClient, optional kotlin.jvm.functions.Function1? factory); method @androidx.compose.runtime.Composable public static com.google.accompanist.web.WebViewNavigator rememberWebViewNavigator(optional kotlinx.coroutines.CoroutineScope coroutineScope); method @androidx.compose.runtime.Composable public static com.google.accompanist.web.WebViewState rememberWebViewState(String url, optional java.util.Map additionalHttpHeaders); - method @androidx.compose.runtime.Composable public static com.google.accompanist.web.WebViewState rememberWebViewStateWithHTMLData(String data, optional String? baseUrl); + method @androidx.compose.runtime.Composable public static com.google.accompanist.web.WebViewState rememberWebViewStateWithHTMLData(String data, optional String? baseUrl, optional String encoding, optional String? mimeType, optional String? historyUrl); } @androidx.compose.runtime.Stable public final class WebViewNavigator { ctor public WebViewNavigator(kotlinx.coroutines.CoroutineScope coroutineScope); method public boolean getCanGoBack(); method public boolean getCanGoForward(); + method public void loadHtml(String html, optional String? baseUrl); + method public void loadUrl(String url, optional java.util.Map additionalHttpHeaders); method public void navigateBack(); method public void navigateForward(); method public void reload(); @@ -97,6 +108,7 @@ package com.google.accompanist.web { ctor public WebViewState(com.google.accompanist.web.WebContent webContent); method public com.google.accompanist.web.WebContent getContent(); method public androidx.compose.runtime.snapshots.SnapshotStateList getErrorsForCurrentRequest(); + method public String? getLastLoadedUrl(); method public com.google.accompanist.web.LoadingState getLoadingState(); method public android.graphics.Bitmap? getPageIcon(); method public String? getPageTitle(); @@ -105,6 +117,7 @@ package com.google.accompanist.web { property public final com.google.accompanist.web.WebContent content; property public final androidx.compose.runtime.snapshots.SnapshotStateList errorsForCurrentRequest; property public final boolean isLoading; + property public final String? lastLoadedUrl; property public final com.google.accompanist.web.LoadingState loadingState; property public final android.graphics.Bitmap? pageIcon; property public final String? pageTitle; diff --git a/web/src/androidTest/assets/test_link.html b/web/src/androidTest/assets/test_link.html new file mode 100644 index 000000000..0d0106852 --- /dev/null +++ b/web/src/androidTest/assets/test_link.html @@ -0,0 +1,25 @@ + + + + + + + Test link + + + + \ No newline at end of file diff --git a/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt b/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt index b518bc099..c3f388559 100644 --- a/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt +++ b/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertHeightIsAtLeast import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.dp import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches import androidx.test.espresso.web.model.Atoms.getCurrentUrl @@ -71,7 +72,7 @@ import java.util.concurrent.TimeUnit // Emulator image doesn't have a WebView until API 26 // Google API emulator image seems to be really flaky before 28 so currently we will set these tests // to min 29 and max 30. 31/32 image is also really flaky -@SdkSuppress(minSdkVersion = 28, maxSdkVersion = 30) +@SdkSuppress(minSdkVersion = 28) class WebTest { @get:Rule val rule = createComposeRule() @@ -148,7 +149,7 @@ class WebTest { // Wait for the webview to load and then perform the check rule.waitForIdle() onWebView().check(webMatches(getCurrentUrl(), containsString(LINK_URL))) - assertThat(state.content.getCurrentUrl()) + assertThat(state.lastLoadedUrl) .isEqualTo(LINK_URL) } @@ -326,7 +327,7 @@ class WebTest { rule.waitForIdle() onWebView().check(webMatches(getCurrentUrl(), containsString("about:blank"))) - assertThat(state.content.getCurrentUrl()) + assertThat(state.lastLoadedUrl) .isEqualTo("about:blank") } @@ -349,7 +350,7 @@ class WebTest { // Wait for the webview to load and then perform the check rule.waitForIdle() onWebView().check(webMatches(getCurrentUrl(), containsString(LINK_URL))) - assertThat(state.content.getCurrentUrl()) + assertThat(state.lastLoadedUrl) .isEqualTo(LINK_URL) } @@ -447,13 +448,14 @@ class WebTest { mockServer.shutdown() } + @FlakyTest @Test fun testNavigatorBack() { lateinit var state: WebViewState lateinit var navigator: WebViewNavigator rule.setContent { - state = rememberWebViewStateWithHTMLData(data = TEST_DATA) + state = rememberWebViewState(url = TEST_LINK_FILE_URL) navigator = rememberWebViewNavigator() WebTestContent( @@ -465,19 +467,18 @@ class WebTest { rule.waitForIdle() - onWebView() - .withElement(findElement(Locator.ID, "link")) - .perform(webClick()) + // HACK: EspressoWeb webClick doesn't track back navigation for some reason + // so manually click the view + rule.onNodeWithTag(WebViewTag).performClick() rule.waitUntil { navigator.canGoBack } - assertThat(state.content.getCurrentUrl()).isEqualTo(LINK_URL) + assertThat(state.lastLoadedUrl).isEqualTo(LINK_URL) navigator.navigateBack() // Check that we're back on the original page with the link onWebView() - .withElement(findElement(Locator.ID, "link")) - .check(webMatches(getText(), equalTo(LINK_TEXT))) + .withElement(findElement(Locator.LINK_TEXT, TEST_LINK_FILE_TEXT)) } @FlakyTest @@ -487,7 +488,7 @@ class WebTest { lateinit var navigator: WebViewNavigator rule.setContent { - state = rememberWebViewStateWithHTMLData(data = TEST_DATA) + state = rememberWebViewState(url = TEST_LINK_FILE_URL) navigator = rememberWebViewNavigator() WebTestContent( @@ -499,23 +500,24 @@ class WebTest { rule.waitForIdle() - onWebView() - .withElement(findElement(Locator.ID, "link")) - .perform(webClick()) + // HACK: EspressoWeb webClick doesn't track back navigation for some reason + // so manually click the view + rule.onNodeWithTag(WebViewTag).performClick() rule.waitUntil { navigator.canGoBack } + assertThat(state.lastLoadedUrl).isEqualTo(LINK_URL) + navigator.navigateBack() // Check that we're back on the original page with the link onWebView() - .withElement(findElement(Locator.ID, "link")) - .check(webMatches(getText(), equalTo(LINK_TEXT))) + .withElement(findElement(Locator.LINK_TEXT, TEST_LINK_FILE_TEXT)) navigator.navigateForward() rule.waitUntil { navigator.canGoBack } rule.waitForIdle() - assertThat(state.content.getCurrentUrl()).isEqualTo(LINK_URL) + assertThat(state.lastLoadedUrl).isEqualTo(LINK_URL) } @Test @@ -524,7 +526,7 @@ class WebTest { lateinit var navigator: WebViewNavigator rule.setContent { - state = rememberWebViewStateWithHTMLData(data = TEST_DATA) + state = rememberWebViewState(url = TEST_LINK_FILE_URL) navigator = rememberWebViewNavigator() WebTestContent( @@ -535,11 +537,10 @@ class WebTest { } rule.waitForIdle() - assertThat(navigator.canGoBack).isFalse() - onWebView() - .withElement(findElement(Locator.ID, "link")) - .perform(webClick()) + // HACK: EspressoWeb webClick doesn't track back navigation for some reason + // so manually click the view + rule.onNodeWithTag(WebViewTag).performClick() rule.waitUntil { navigator.canGoBack } assertThat(navigator.canGoBack).isTrue() @@ -551,7 +552,7 @@ class WebTest { lateinit var navigator: WebViewNavigator rule.setContent { - state = rememberWebViewStateWithHTMLData(data = TEST_DATA) + state = rememberWebViewState(url = TEST_LINK_FILE_URL) navigator = rememberWebViewNavigator() WebTestContent( @@ -563,9 +564,9 @@ class WebTest { rule.waitForIdle() - onWebView() - .withElement(findElement(Locator.ID, "link")) - .perform(webClick()) + // HACK: EspressoWeb webClick doesn't track back navigation for some reason + // so manually click the view + rule.onNodeWithTag(WebViewTag).performClick() rule.waitUntil { navigator.canGoBack } navigator.navigateBack() @@ -721,6 +722,8 @@ class WebTest { private const val LINK_ID = "link" private const val LINK_TEXT = "Click me" private const val LINK_URL = "file:///android_asset/test.html" +private const val TEST_LINK_FILE_URL = "file:///android_asset/test_link.html" +private const val TEST_LINK_FILE_TEXT = "Test link" private const val TITLE_TEXT = "A Test Title" private const val TEST_DATA = "$LINK_TEXT" diff --git a/web/src/main/java/com/google/accompanist/web/WebView.kt b/web/src/main/java/com/google/accompanist/web/WebView.kt index 8dac30b1b..d04700088 100644 --- a/web/src/main/java/com/google/accompanist/web/WebView.kt +++ b/web/src/main/java/com/google/accompanist/web/WebView.kt @@ -38,10 +38,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView +import com.google.accompanist.web.LoadingState.Finished +import com.google.accompanist.web.LoadingState.Loading import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -89,9 +91,31 @@ fun WebView( with(navigator) { webView?.handleNavigationEvents() } } + LaunchedEffect(webView, state) { + if (webView == null) return@LaunchedEffect + + snapshotFlow { state.content }.collect { content -> + when (content) { + is WebContent.Url -> { + webView?.loadUrl(content.url, content.additionalHttpHeaders) + } + + is WebContent.Data -> { + webView?.loadDataWithBaseURL( + content.baseUrl, + content.data, + content.mimeType, + content.encoding, + content.historyUrl + ) + } + } + } + } + val currentOnDispose by rememberUpdatedState(onDispose) - webView?.let { it -> + webView?.let { DisposableEffect(it) { onDispose { currentOnDispose(it) } } @@ -104,8 +128,6 @@ fun WebView( client.navigator = navigator chromeClient.state = state - val runningInPreview = LocalInspectionMode.current - BoxWithConstraints(modifier) { AndroidView( factory = { context -> @@ -135,27 +157,7 @@ fun WebView( webViewClient = client }.also { webView = it } } - ) { view -> - // AndroidViews are not supported by preview, bail early - if (runningInPreview) return@AndroidView - - when (val content = state.content) { - is WebContent.Url -> { - val url = content.url - - if (url.isNotEmpty() && url != view.url) { - view.loadUrl(url, content.additionalHttpHeaders.toMutableMap()) - } - } - - is WebContent.Data -> { - view.loadDataWithBaseURL(content.baseUrl, content.data, null, "utf-8", null) - } - } - - navigator.canGoBack = view.canGoBack() - navigator.canGoForward = view.canGoForward() - } + ) } } @@ -179,33 +181,20 @@ open class AccompanistWebViewClient : WebViewClient() { state.errorsForCurrentRequest.clear() state.pageTitle = null state.pageIcon = null + + state.lastLoadedUrl = url } override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) state.loadingState = LoadingState.Finished - navigator.canGoBack = view?.canGoBack() ?: false - navigator.canGoForward = view?.canGoForward() ?: false } - override fun doUpdateVisitedHistory( - view: WebView?, - url: String?, - isReload: Boolean - ) { + override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) { super.doUpdateVisitedHistory(view, url, isReload) - // WebView will often update the current url itself. - // This happens in situations like redirects and navigating through - // history. We capture this change and update our state holder url. - // On older APIs (28 and lower), this method is called when loading - // html data. We don't want to update the state in this case as that will - // overwrite the html being loaded. - if (url != null && - !url.startsWith("data:text/html") && - state.content.getCurrentUrl() != url - ) { - state.content = state.content.withUrl(url) - } + + navigator.canGoBack = view?.canGoBack() ?: false + navigator.canGoForward = view?.canGoForward() ?: false } override fun onReceivedError( @@ -219,24 +208,6 @@ open class AccompanistWebViewClient : WebViewClient() { state.errorsForCurrentRequest.add(WebViewError(request, error)) } } - - override fun shouldOverrideUrlLoading( - view: WebView?, - request: WebResourceRequest? - ): Boolean { - // If the url hasn't changed, this is probably an internal event like - // a javascript reload. We should let it happen. - if (view?.url == request?.url.toString()) { - return false - } - - // Override all url loads to make the single source of truth - // of the URL the state holder Url - request?.let { - state.content = state.content.withUrl(it.url.toString()) - } - return true - } } /** @@ -274,8 +245,15 @@ sealed class WebContent { val additionalHttpHeaders: Map = emptyMap(), ) : WebContent() - data class Data(val data: String, val baseUrl: String? = null) : WebContent() + data class Data( + val data: String, + val baseUrl: String? = null, + val encoding: String = "utf-8", + val mimeType: String? = null, + val historyUrl: String? = null + ) : WebContent() + @Deprecated("Use state.lastLoadedUrl instead") fun getCurrentUrl(): String? { return when (this) { is Url -> url @@ -317,6 +295,9 @@ sealed class LoadingState { */ @Stable class WebViewState(webContent: WebContent) { + var lastLoadedUrl by mutableStateOf(null) + internal set + /** * The content being loaded by the WebView */ @@ -364,7 +345,19 @@ class WebViewState(webContent: WebContent) { @Stable class WebViewNavigator(private val coroutineScope: CoroutineScope) { - private enum class NavigationEvent { BACK, FORWARD, RELOAD, STOP_LOADING } + private sealed interface NavigationEvent { + object Back : NavigationEvent + object Forward : NavigationEvent + object Reload : NavigationEvent + object StopLoading : NavigationEvent + + data class LoadUrl( + val url: String, + val additionalHttpHeaders: Map = emptyMap() + ) : NavigationEvent + + data class LoadHtml(val html: String, val baseUrl: String? = null) : NavigationEvent + } private val navigationEvents: MutableSharedFlow = MutableSharedFlow() @@ -372,10 +365,21 @@ class WebViewNavigator(private val coroutineScope: CoroutineScope) { internal suspend fun WebView.handleNavigationEvents(): Nothing = withContext(Dispatchers.Main) { navigationEvents.collect { event -> when (event) { - NavigationEvent.BACK -> goBack() - NavigationEvent.FORWARD -> goForward() - NavigationEvent.RELOAD -> reload() - NavigationEvent.STOP_LOADING -> stopLoading() + is NavigationEvent.Back -> goBack() + is NavigationEvent.Forward -> goForward() + is NavigationEvent.Reload -> reload() + is NavigationEvent.StopLoading -> stopLoading() + is NavigationEvent.LoadHtml -> loadDataWithBaseURL( + event.baseUrl, + event.html, + null, + "utf-8", + null + ) + + is NavigationEvent.LoadUrl -> { + loadUrl(event.url, event.additionalHttpHeaders) + } } } } @@ -392,32 +396,47 @@ class WebViewNavigator(private val coroutineScope: CoroutineScope) { var canGoForward: Boolean by mutableStateOf(false) internal set + fun loadUrl(url: String, additionalHttpHeaders: Map = emptyMap()) { + coroutineScope.launch { + navigationEvents.emit( + NavigationEvent.LoadUrl( + url, + additionalHttpHeaders + ) + ) + } + } + + fun loadHtml(html: String, baseUrl: String? = null) { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.LoadHtml(html, baseUrl)) } + } + /** * Navigates the webview back to the previous page. */ fun navigateBack() { - coroutineScope.launch { navigationEvents.emit(NavigationEvent.BACK) } + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) } } /** * Navigates the webview forward after going back from a page. */ fun navigateForward() { - coroutineScope.launch { navigationEvents.emit(NavigationEvent.FORWARD) } + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) } } /** * Reloads the current page in the webview. */ fun reload() { - coroutineScope.launch { navigationEvents.emit(NavigationEvent.RELOAD) } + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) } } /** * Stops the current page load (if one is loading). */ fun stopLoading() { - coroutineScope.launch { navigationEvents.emit(NavigationEvent.STOP_LOADING) } + coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) } } } @@ -459,13 +478,18 @@ fun rememberWebViewState( ): WebViewState = // Rather than using .apply {} here we will recreate the state, this prevents // a recomposition loop when the webview updates the url itself. - remember(url, additionalHttpHeaders) { + remember { WebViewState( WebContent.Url( url = url, additionalHttpHeaders = additionalHttpHeaders ) ) + }.apply { + this.content = WebContent.Url( + url = url, + additionalHttpHeaders = additionalHttpHeaders + ) } /** @@ -474,7 +498,17 @@ fun rememberWebViewState( * @param data The uri to load in the WebView */ @Composable -fun rememberWebViewStateWithHTMLData(data: String, baseUrl: String? = null): WebViewState = - remember(data, baseUrl) { - WebViewState(WebContent.Data(data, baseUrl)) +fun rememberWebViewStateWithHTMLData( + data: String, + baseUrl: String? = null, + encoding: String = "utf-8", + mimeType: String? = null, + historyUrl: String? = null +): WebViewState = + remember { + WebViewState(WebContent.Data(data, baseUrl, encoding, mimeType, historyUrl)) + }.apply { + this.content = WebContent.Data( + data, baseUrl, encoding, mimeType, historyUrl + ) } From cf3dec1c1ddab9281dcc394c73b0bf95a8bdecab Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Tue, 17 Jan 2023 11:47:33 +1100 Subject: [PATCH 003/110] Update api --- web/api/current.api | 6 ++-- .../com/google/accompanist/web/WebTest.kt | 2 ++ .../com/google/accompanist/web/WebView.kt | 34 +++++++++++++++---- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/web/api/current.api b/web/api/current.api index baceb0d40..7a8e7e054 100644 --- a/web/api/current.api +++ b/web/api/current.api @@ -94,7 +94,7 @@ package com.google.accompanist.web { ctor public WebViewNavigator(kotlinx.coroutines.CoroutineScope coroutineScope); method public boolean getCanGoBack(); method public boolean getCanGoForward(); - method public void loadHtml(String html, optional String? baseUrl); + method public void loadHtml(String html, optional String? baseUrl, optional String? mimeType, optional String? encoding, optional String? historyUrl); method public void loadUrl(String url, optional java.util.Map additionalHttpHeaders); method public void navigateBack(); method public void navigateForward(); @@ -108,7 +108,7 @@ package com.google.accompanist.web { ctor public WebViewState(com.google.accompanist.web.WebContent webContent); method public com.google.accompanist.web.WebContent getContent(); method public androidx.compose.runtime.snapshots.SnapshotStateList getErrorsForCurrentRequest(); - method public String? getLastLoadedUrl(); + method public error.NonExistentClass! getLastLoadedUrl(); method public com.google.accompanist.web.LoadingState getLoadingState(); method public android.graphics.Bitmap? getPageIcon(); method public String? getPageTitle(); @@ -117,7 +117,7 @@ package com.google.accompanist.web { property public final com.google.accompanist.web.WebContent content; property public final androidx.compose.runtime.snapshots.SnapshotStateList errorsForCurrentRequest; property public final boolean isLoading; - property public final String? lastLoadedUrl; + property public final error.NonExistentClass! lastLoadedUrl; property public final com.google.accompanist.web.LoadingState loadingState; property public final android.graphics.Bitmap? pageIcon; property public final String? pageTitle; diff --git a/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt b/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt index c3f388559..54f741020 100644 --- a/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt +++ b/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt @@ -520,6 +520,7 @@ class WebTest { assertThat(state.lastLoadedUrl).isEqualTo(LINK_URL) } + @FlakyTest @Test fun testNavigatorCanGoBack() { lateinit var state: WebViewState @@ -546,6 +547,7 @@ class WebTest { assertThat(navigator.canGoBack).isTrue() } + @FlakyTest @Test fun testNavigatorCanGoForward() { lateinit var state: WebViewState diff --git a/web/src/main/java/com/google/accompanist/web/WebView.kt b/web/src/main/java/com/google/accompanist/web/WebView.kt index d04700088..de3b5d5a4 100644 --- a/web/src/main/java/com/google/accompanist/web/WebView.kt +++ b/web/src/main/java/com/google/accompanist/web/WebView.kt @@ -356,7 +356,13 @@ class WebViewNavigator(private val coroutineScope: CoroutineScope) { val additionalHttpHeaders: Map = emptyMap() ) : NavigationEvent - data class LoadHtml(val html: String, val baseUrl: String? = null) : NavigationEvent + data class LoadHtml( + val html: String, + val baseUrl: String? = null, + val mimeType: String? = null, + val encoding: String? = "utf-8", + val historyUrl: String? = null + ) : NavigationEvent } private val navigationEvents: MutableSharedFlow = MutableSharedFlow() @@ -372,9 +378,9 @@ class WebViewNavigator(private val coroutineScope: CoroutineScope) { is NavigationEvent.LoadHtml -> loadDataWithBaseURL( event.baseUrl, event.html, - null, - "utf-8", - null + event.mimeType, + event.encoding, + event.historyUrl ) is NavigationEvent.LoadUrl -> { @@ -407,8 +413,24 @@ class WebViewNavigator(private val coroutineScope: CoroutineScope) { } } - fun loadHtml(html: String, baseUrl: String? = null) { - coroutineScope.launch { navigationEvents.emit(NavigationEvent.LoadHtml(html, baseUrl)) } + fun loadHtml( + html: String, + baseUrl: String? = null, + mimeType: String? = null, + encoding: String? = "utf-8", + historyUrl: String? = null + ) { + coroutineScope.launch { + navigationEvents.emit( + NavigationEvent.LoadHtml( + html, + baseUrl, + mimeType, + encoding, + historyUrl + ) + ) + } } /** From 6715daded6a60f0f80df948e8989a300f21777e1 Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Wed, 18 Jan 2023 08:32:34 +1100 Subject: [PATCH 004/110] Extract test method --- .../com/google/accompanist/web/WebTest.kt | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt b/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt index 54f741020..2c9d19a17 100644 --- a/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt +++ b/web/src/androidTest/kotlin/com/google/accompanist/web/WebTest.kt @@ -71,7 +71,7 @@ import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) // Emulator image doesn't have a WebView until API 26 // Google API emulator image seems to be really flaky before 28 so currently we will set these tests -// to min 29 and max 30. 31/32 image is also really flaky +// to min 29. @SdkSuppress(minSdkVersion = 28) class WebTest { @get:Rule @@ -466,10 +466,7 @@ class WebTest { } rule.waitForIdle() - - // HACK: EspressoWeb webClick doesn't track back navigation for some reason - // so manually click the view - rule.onNodeWithTag(WebViewTag).performClick() + clickOnWebViewLink() rule.waitUntil { navigator.canGoBack } assertThat(state.lastLoadedUrl).isEqualTo(LINK_URL) @@ -499,10 +496,7 @@ class WebTest { } rule.waitForIdle() - - // HACK: EspressoWeb webClick doesn't track back navigation for some reason - // so manually click the view - rule.onNodeWithTag(WebViewTag).performClick() + clickOnWebViewLink() rule.waitUntil { navigator.canGoBack } assertThat(state.lastLoadedUrl).isEqualTo(LINK_URL) @@ -538,10 +532,7 @@ class WebTest { } rule.waitForIdle() - - // HACK: EspressoWeb webClick doesn't track back navigation for some reason - // so manually click the view - rule.onNodeWithTag(WebViewTag).performClick() + clickOnWebViewLink() rule.waitUntil { navigator.canGoBack } assertThat(navigator.canGoBack).isTrue() @@ -565,10 +556,7 @@ class WebTest { } rule.waitForIdle() - - // HACK: EspressoWeb webClick doesn't track back navigation for some reason - // so manually click the view - rule.onNodeWithTag(WebViewTag).performClick() + clickOnWebViewLink() rule.waitUntil { navigator.canGoBack } navigator.navigateBack() @@ -719,6 +707,12 @@ class WebTest { // If the WebView is wrapping it's content successfully, the box will have some height. rule.onNodeWithTag("box").assertHeightIsAtLeast(1.dp) } + + private fun clickOnWebViewLink() { + // HACK: EspressoWeb webClick doesn't track back navigation for some reason + // so manually click the view + rule.onNodeWithTag(WebViewTag).performClick() + } } private const val LINK_ID = "link" From 297def983080b651effa73d3dafe64bacaa205cf Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Wed, 18 Jan 2023 10:52:13 +1100 Subject: [PATCH 005/110] Wrap webview in a viewgroup to fix rare crash --- .../main/java/com/google/accompanist/web/WebView.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/src/main/java/com/google/accompanist/web/WebView.kt b/web/src/main/java/com/google/accompanist/web/WebView.kt index de3b5d5a4..ebf243f5c 100644 --- a/web/src/main/java/com/google/accompanist/web/WebView.kt +++ b/web/src/main/java/com/google/accompanist/web/WebView.kt @@ -24,6 +24,7 @@ import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient +import android.widget.FrameLayout import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.runtime.Composable @@ -131,7 +132,7 @@ fun WebView( BoxWithConstraints(modifier) { AndroidView( factory = { context -> - (factory?.invoke(context) ?: WebView(context)).apply { + val childView = (factory?.invoke(context) ?: WebView(context)).apply { onCreated(this) // WebView changes it's layout strategy based on @@ -156,6 +157,14 @@ fun WebView( webChromeClient = chromeClient webViewClient = client }.also { webView = it } + + // Workaround a crash on certain devices that expect WebView to be + // wrapped in a ViewGroup. + // b/243567497 + val parentLayout = FrameLayout(context) + parentLayout.addView(childView) + + parentLayout } ) } From c7306a3ddaf845ce2d1c2cd7f6dfcab017f26b1e Mon Sep 17 00:00:00 2001 From: Levi Date: Thu, 19 Jan 2023 11:34:57 +0000 Subject: [PATCH 006/110] [Pager Indicators] Update Indicators to also be usable with Foundation Pager (#1485) * Update pager indicators to support foundation PagerState * Change FoundationTabIndicatorTest name to TabIndicatorWithFoundationPagerTest. * Clean up formatting * Update API docs * Apply code review feedback * Apply code review feedback - second round --- pager-indicators/api/current.api | 3 + .../TabIndicatorWithFoundationPagerTest.kt | 180 ++++++++++++++ .../accompanist/pager/PagerIndicator.kt | 224 +++++++++++++++++- .../com/google/accompanist/pager/PagerTab.kt | 44 +++- 4 files changed, 444 insertions(+), 7 deletions(-) create mode 100644 pager-indicators/src/androidTest/kotlin/com/google/accompanist/pager/TabIndicatorWithFoundationPagerTest.kt diff --git a/pager-indicators/api/current.api b/pager-indicators/api/current.api index 1ea8a99c0..9fc70286e 100644 --- a/pager-indicators/api/current.api +++ b/pager-indicators/api/current.api @@ -3,11 +3,14 @@ package com.google.accompanist.pager { public final class PagerIndicatorKt { method @androidx.compose.runtime.Composable @com.google.accompanist.pager.ExperimentalPagerApi public static void HorizontalPagerIndicator(com.google.accompanist.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional int pageCount, optional kotlin.jvm.functions.Function1 pageIndexMapping, optional long activeColor, optional long inactiveColor, optional float indicatorWidth, optional float indicatorHeight, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape); + method @androidx.compose.runtime.Composable @com.google.accompanist.pager.ExperimentalPagerApi public static void HorizontalPagerIndicator(androidx.compose.foundation.pager.PagerState pagerState, int pageCount, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1 pageIndexMapping, optional long activeColor, optional long inactiveColor, optional float indicatorWidth, optional float indicatorHeight, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape); method @androidx.compose.runtime.Composable @com.google.accompanist.pager.ExperimentalPagerApi public static void VerticalPagerIndicator(com.google.accompanist.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional int pageCount, optional kotlin.jvm.functions.Function1 pageIndexMapping, optional long activeColor, optional long inactiveColor, optional float indicatorHeight, optional float indicatorWidth, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape); + method @androidx.compose.runtime.Composable @com.google.accompanist.pager.ExperimentalPagerApi public static void VerticalPagerIndicator(androidx.compose.foundation.pager.PagerState pagerState, int pageCount, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1 pageIndexMapping, optional long activeColor, optional long inactiveColor, optional float indicatorHeight, optional float indicatorWidth, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape); } public final class PagerTabKt { method @com.google.accompanist.pager.ExperimentalPagerApi public static androidx.compose.ui.Modifier pagerTabIndicatorOffset(androidx.compose.ui.Modifier, com.google.accompanist.pager.PagerState pagerState, java.util.List tabPositions, optional kotlin.jvm.functions.Function1 pageIndexMapping); + method @com.google.accompanist.pager.ExperimentalPagerApi public static androidx.compose.ui.Modifier pagerTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.foundation.pager.PagerState pagerState, java.util.List tabPositions, optional kotlin.jvm.functions.Function1 pageIndexMapping); } } diff --git a/pager-indicators/src/androidTest/kotlin/com/google/accompanist/pager/TabIndicatorWithFoundationPagerTest.kt b/pager-indicators/src/androidTest/kotlin/com/google/accompanist/pager/TabIndicatorWithFoundationPagerTest.kt new file mode 100644 index 000000000..4ba3dbcae --- /dev/null +++ b/pager-indicators/src/androidTest/kotlin/com/google/accompanist/pager/TabIndicatorWithFoundationPagerTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.pager + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ScrollableTabRow +import androidx.compose.material.Tab +import androidx.compose.material.TabRowDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.getBoundsInRoot +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.height +import androidx.compose.ui.unit.lerp +import androidx.compose.ui.unit.times +import androidx.compose.ui.unit.width +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import androidx.compose.foundation.pager.HorizontalPager as FoundationHorizontalPager +import androidx.compose.foundation.pager.PagerState as FoundationPagerState +import androidx.compose.foundation.pager.rememberPagerState as rememberFoundationPagerState + +@OptIn(ExperimentalFoundationApi::class) +@RunWith(AndroidJUnit4::class) +class TabIndicatorWithFoundationPagerTest { + @get:Rule + val rule = createComposeRule() + + private val IndicatorTag = "indicator" + private val TabRowTag = "TabRow" + + @Test + fun emptyPager() { + rule.setContent { + val pagerState = rememberFoundationPagerState() + TabRow(pagerState, 0) + } + } + + @Test + fun scrollOffsetIsPositive() { + lateinit var pagerState: FoundationPagerState + val pageCount = 4 + rule.setContent { + pagerState = rememberFoundationPagerState() + Column { + TabRow(pagerState, pageCount) + FoundationHorizontalPager(pageCount = pageCount, state = pagerState) { + Box(Modifier.fillMaxSize()) + } + } + } + + rule.runOnIdle { + runBlocking { pagerState.scrollToPage(1, 0.25f) } + } + + val tab1Bounds = rule.onNodeWithTag("1").getBoundsInRoot() + val tab2Bounds = rule.onNodeWithTag("2").getBoundsInRoot() + val indicatorBounds = rule.onNodeWithTag(IndicatorTag).getBoundsInRoot() + + with(rule.density) { + assertThat(indicatorBounds.left.roundToPx()) + .isEqualTo(lerp(tab1Bounds.left, tab2Bounds.left, 0.25f).roundToPx()) + assertThat(indicatorBounds.width.roundToPx()) + .isEqualTo(lerp(tab1Bounds.width, tab2Bounds.width, 0.25f).roundToPx()) + } + } + + @Test + fun scrollOffsetIsNegative() { + lateinit var pagerState: FoundationPagerState + val pageCount = 4 + rule.setContent { + pagerState = rememberFoundationPagerState() + Column { + TabRow(pagerState, pageCount) + FoundationHorizontalPager(pageCount = pageCount, state = pagerState) { + Box(Modifier.fillMaxSize()) + } + } + } + + rule.runOnIdle { + runBlocking { pagerState.scrollToPage(1, -0.25f) } + } + + val tab1Bounds = rule.onNodeWithTag("1").getBoundsInRoot() + val tab0Bounds = rule.onNodeWithTag("0").getBoundsInRoot() + val indicatorBounds = rule.onNodeWithTag(IndicatorTag).getBoundsInRoot() + + with(rule.density) { + assertThat(indicatorBounds.left.roundToPx()) + .isEqualTo(lerp(tab1Bounds.left, tab0Bounds.left, 0.25f).roundToPx()) + assertThat(indicatorBounds.width.roundToPx()) + .isEqualTo(lerp(tab1Bounds.width, tab0Bounds.width, 0.25f).roundToPx()) + } + } + + @Test + fun indicatorIsAtBottom() { + lateinit var pagerState: FoundationPagerState + val pageCount = 4 + rule.setContent { + pagerState = rememberFoundationPagerState() + Column { + TabRow(pagerState, pageCount) + FoundationHorizontalPager(pageCount = pageCount, state = pagerState) { + Box(Modifier.fillMaxSize()) + } + } + } + + rule.runOnIdle { + runBlocking { pagerState.scrollToPage(1, 0.25f) } + } + + val tabRowBounds = rule.onNodeWithTag(TabRowTag).getBoundsInRoot() + + val indicatorBounds = rule.onNodeWithTag(IndicatorTag).getBoundsInRoot() + + with(rule.density) { + assertThat(indicatorBounds.height.roundToPx()).isEqualTo(2.dp.roundToPx()) + assertThat(indicatorBounds.bottom).isEqualTo(tabRowBounds.bottom) + } + } + + @OptIn(ExperimentalPagerApi::class) + @Composable + private fun TabRow(pagerState: FoundationPagerState, pageCount: Int) { + ScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier + .pagerTabIndicatorOffset(pagerState, tabPositions) + .testTag(IndicatorTag), + height = 2.dp + ) + }, + modifier = Modifier.testTag(TabRowTag) + ) { + // Add tabs for all of our pages + (0 until pageCount).forEach { index -> + Tab( + text = { Text("Tab $index", Modifier.padding(horizontal = index * 5.dp)) }, + selected = pagerState.currentPage == index, + modifier = Modifier.testTag("$index"), + onClick = {} + ) + } + } + } +} diff --git a/pager-indicators/src/main/java/com/google/accompanist/pager/PagerIndicator.kt b/pager-indicators/src/main/java/com/google/accompanist/pager/PagerIndicator.kt index 0731a080e..1fdd3575d 100644 --- a/pager-indicators/src/main/java/com/google/accompanist/pager/PagerIndicator.kt +++ b/pager-indicators/src/main/java/com/google/accompanist/pager/PagerIndicator.kt @@ -16,6 +16,7 @@ package com.google.accompanist.pager +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -28,6 +29,7 @@ import androidx.compose.material.ContentAlpha import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -40,7 +42,7 @@ import kotlin.math.absoluteValue import kotlin.math.sign /** - * An horizontally laid out indicator for a [HorizontalPager] or [VerticalPager], representing + * A horizontally laid out indicator for a [HorizontalPager] or [VerticalPager], representing * the currently active page and total pages drawn using a [Shape]. * * This element allows the setting of the [indicatorShape], which defines how the @@ -48,7 +50,7 @@ import kotlin.math.sign * * @sample com.google.accompanist.sample.pager.HorizontalPagerIndicatorSample * - * @param pagerState the state object of your [Pager] to be used to observe the list's state. + * @param pagerState the state object of your pager to be used to observe the list's state. * @param modifier the modifier to apply to this layout. * @param pageCount the size of indicators should be displayed, defaults to [PagerState.pageCount]. * If you are implementing a looping pager with a much larger [PagerState.pageCount] @@ -77,6 +79,107 @@ fun HorizontalPagerIndicator( spacing: Dp = indicatorWidth, indicatorShape: Shape = CircleShape, ) { + val stateBridge = remember(pagerState) { + object : PagerStateBridge { + override val currentPage: Int + get() = pagerState.currentPage + override val currentPageOffset: Float + get() = pagerState.currentPageOffset + } + } + + HorizontalPagerIndicator( + pagerState = stateBridge, + pageCount = pageCount, + modifier = modifier, + pageIndexMapping = pageIndexMapping, + activeColor = activeColor, + inactiveColor = inactiveColor, + indicatorHeight = indicatorHeight, + indicatorWidth = indicatorWidth, + spacing = spacing, + indicatorShape = indicatorShape + ) +} + +/** + * A horizontally laid out indicator for a [androidx.compose.foundation.pager.HorizontalPager] or + * [androidx.compose.foundation.pager.VerticalPager], representing + * the currently active page and total pages drawn using a [Shape]. + * + * This element allows the setting of the [indicatorShape], which defines how the + * indicator is visually represented. + * + * @sample com.google.accompanist.sample.pager.HorizontalPagerIndicatorSample + * + * @param pagerState A [androidx.compose.foundation.pager.PagerState] object of your + * [androidx.compose.foundation.pager.VerticalPager] or + * [androidx.compose.foundation.pager.HorizontalPager]to be used to observe the list's state. + * @param modifier the modifier to apply to this layout. + * @param pageCount the size of indicators should be displayed. + * If you are implementing a looping pager with a much larger [pageCount] + * than indicators should displayed, e.g. [Int.MAX_VALUE], specify you real size in this param. + * @param pageIndexMapping describe how to get the position of active indicator by the giving page + * from [androidx.compose.foundation.pager.PagerState.currentPage]. + * @param activeColor the color of the active Page indicator + * @param inactiveColor the color of page indicators that are inactive. This defaults to + * [activeColor] with the alpha component set to the [ContentAlpha.disabled]. + * @param indicatorWidth the width of each indicator in [Dp]. + * @param indicatorHeight the height of each indicator in [Dp]. Defaults to [indicatorWidth]. + * @param spacing the spacing between each indicator in [Dp]. + * @param indicatorShape the shape representing each indicator. This defaults to [CircleShape]. + */ +@OptIn(ExperimentalFoundationApi::class) +@ExperimentalPagerApi +@Composable +fun HorizontalPagerIndicator( + pagerState: androidx.compose.foundation.pager.PagerState, + pageCount: Int, + modifier: Modifier = Modifier, + pageIndexMapping: (Int) -> Int = { it }, + activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + inactiveColor: Color = activeColor.copy(ContentAlpha.disabled), + indicatorWidth: Dp = 8.dp, + indicatorHeight: Dp = indicatorWidth, + spacing: Dp = indicatorWidth, + indicatorShape: Shape = CircleShape, +) { + val stateBridge = remember(pagerState) { + object : PagerStateBridge { + override val currentPage: Int + get() = pagerState.currentPage + override val currentPageOffset: Float + get() = pagerState.currentPageOffsetFraction + } + } + + HorizontalPagerIndicator( + pagerState = stateBridge, + pageCount = pageCount, + modifier = modifier, + pageIndexMapping = pageIndexMapping, + activeColor = activeColor, + inactiveColor = inactiveColor, + indicatorHeight = indicatorHeight, + indicatorWidth = indicatorWidth, + spacing = spacing, + indicatorShape = indicatorShape + ) +} + +@Composable +private fun HorizontalPagerIndicator( + pagerState: PagerStateBridge, + pageCount: Int, + modifier: Modifier = Modifier, + pageIndexMapping: (Int) -> Int = { it }, + activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + inactiveColor: Color = activeColor.copy(ContentAlpha.disabled), + indicatorWidth: Dp = 8.dp, + indicatorHeight: Dp = indicatorWidth, + spacing: Dp = indicatorWidth, + indicatorShape: Shape = CircleShape, +) { val indicatorWidthPx = LocalDensity.current.run { indicatorWidth.roundToPx() } val spacingPx = LocalDensity.current.run { spacing.roundToPx() } @@ -105,7 +208,12 @@ fun HorizontalPagerIndicator( val offset = pagerState.currentPageOffset val next = pageIndexMapping(pagerState.currentPage + offset.sign.toInt()) val scrollPosition = ((next - position) * offset.absoluteValue + position) - .coerceIn(0f, (pageCount - 1).coerceAtLeast(0).toFloat()) + .coerceIn( + 0f, + (pageCount - 1) + .coerceAtLeast(0) + .toFloat() + ) IntOffset( x = ((spacingPx + indicatorWidthPx) * scrollPosition).toInt(), @@ -125,7 +233,7 @@ fun HorizontalPagerIndicator( } /** - * An vertically laid out indicator for a [VerticalPager] or [HorizontalPager], representing + * A vertically laid out indicator for a [VerticalPager] or [HorizontalPager], representing * the currently active page and total pages drawn using a [Shape]. * * This element allows the setting of the [indicatorShape], which defines how the @@ -133,7 +241,7 @@ fun HorizontalPagerIndicator( * * @sample com.google.accompanist.sample.pager.VerticalPagerIndicatorSample * - * @param pagerState the state object of your [Pager] to be used to observe the list's state. + * @param pagerState the state object of your pager to be used to observe the list's state. * @param modifier the modifier to apply to this layout. * @param pageCount the size of indicators should be displayed, defaults to [PagerState.pageCount]. * If you are implementing a looping pager with a much larger [PagerState.pageCount] @@ -162,6 +270,105 @@ fun VerticalPagerIndicator( spacing: Dp = indicatorHeight, indicatorShape: Shape = CircleShape, ) { + val stateBridge = remember(pagerState) { + object : PagerStateBridge { + override val currentPage: Int + get() = pagerState.currentPage + override val currentPageOffset: Float + get() = pagerState.currentPageOffset + } + } + + VerticalPagerIndicator( + pagerState = stateBridge, + pageCount = pageCount, + modifier = modifier, + pageIndexMapping = pageIndexMapping, + activeColor = activeColor, + inactiveColor = inactiveColor, + indicatorHeight = indicatorHeight, + indicatorWidth = indicatorWidth, + spacing = spacing, + indicatorShape = indicatorShape + ) +} + +/** + * A vertically laid out indicator for a [androidx.compose.foundation.pager.VerticalPager] or + * [androidx.compose.foundation.pager.HorizontalPager], representing + * the currently active page and total pages drawn using a [Shape]. + * + * This element allows the setting of the [indicatorShape], which defines how the + * indicator is visually represented. + * + * @param pagerState A [androidx.compose.foundation.pager.PagerState] object of your + * [androidx.compose.foundation.pager.VerticalPager] or + * [androidx.compose.foundation.pager.HorizontalPager]to be used to observe the list's state. + * @param modifier the modifier to apply to this layout. + * @param pageCount the size of indicators should be displayed. If you are implementing a looping + * pager with a much larger [pageCount] than indicators should displayed, e.g. [Int.MAX_VALUE], + * specify you real size in this param. + * @param pageIndexMapping describe how to get the position of active indicator by the giving page + * from [androidx.compose.foundation.pager.PagerState.currentPage]. + * @param activeColor the color of the active Page indicator + * @param inactiveColor the color of page indicators that are inactive. This defaults to + * [activeColor] with the alpha component set to the [ContentAlpha.disabled]. + * @param indicatorHeight the height of each indicator in [Dp]. + * @param indicatorWidth the width of each indicator in [Dp]. Defaults to [indicatorHeight]. + * @param spacing the spacing between each indicator in [Dp]. + * @param indicatorShape the shape representing each indicator. This defaults to [CircleShape]. + */ +@OptIn(ExperimentalFoundationApi::class) +@ExperimentalPagerApi +@Composable +fun VerticalPagerIndicator( + pagerState: androidx.compose.foundation.pager.PagerState, + pageCount: Int, + modifier: Modifier = Modifier, + pageIndexMapping: (Int) -> Int = { it }, + activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + inactiveColor: Color = activeColor.copy(ContentAlpha.disabled), + indicatorHeight: Dp = 8.dp, + indicatorWidth: Dp = indicatorHeight, + spacing: Dp = indicatorHeight, + indicatorShape: Shape = CircleShape, +) { + val stateBridge = remember(pagerState) { + object : PagerStateBridge { + override val currentPage: Int + get() = pagerState.currentPage + override val currentPageOffset: Float + get() = pagerState.currentPageOffsetFraction + } + } + + VerticalPagerIndicator( + pagerState = stateBridge, + pageCount = pageCount, + modifier = modifier, + pageIndexMapping = pageIndexMapping, + activeColor = activeColor, + inactiveColor = inactiveColor, + indicatorHeight = indicatorHeight, + indicatorWidth = indicatorWidth, + spacing = spacing, + indicatorShape = indicatorShape + ) +} + +@Composable +private fun VerticalPagerIndicator( + pagerState: PagerStateBridge, + pageCount: Int, + modifier: Modifier = Modifier, + pageIndexMapping: (Int) -> Int = { it }, + activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current), + inactiveColor: Color = activeColor.copy(ContentAlpha.disabled), + indicatorHeight: Dp = 8.dp, + indicatorWidth: Dp = indicatorHeight, + spacing: Dp = indicatorHeight, + indicatorShape: Shape = CircleShape, +) { val indicatorHeightPx = LocalDensity.current.run { indicatorHeight.roundToPx() } val spacingPx = LocalDensity.current.run { spacing.roundToPx() } @@ -190,7 +397,12 @@ fun VerticalPagerIndicator( val offset = pagerState.currentPageOffset val next = pageIndexMapping(pagerState.currentPage + offset.sign.toInt()) val scrollPosition = ((next - position) * offset.absoluteValue + position) - .coerceIn(0f, (pageCount - 1).coerceAtLeast(0).toFloat()) + .coerceIn( + 0f, + (pageCount - 1) + .coerceAtLeast(0) + .toFloat() + ) IntOffset( x = 0, diff --git a/pager-indicators/src/main/java/com/google/accompanist/pager/PagerTab.kt b/pager-indicators/src/main/java/com/google/accompanist/pager/PagerTab.kt index e6cc3049c..8e304cba7 100644 --- a/pager-indicators/src/main/java/com/google/accompanist/pager/PagerTab.kt +++ b/pager-indicators/src/main/java/com/google/accompanist/pager/PagerTab.kt @@ -16,6 +16,7 @@ package com.google.accompanist.pager +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.material.ScrollableTabRow import androidx.compose.material.TabPosition import androidx.compose.material.TabRow @@ -23,7 +24,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.layout import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.lerp - /** * This indicator syncs up a [TabRow] or [ScrollableTabRow] tab indicator with a * [HorizontalPager] or [VerticalPager]. See the sample for a full demonstration. @@ -35,6 +35,43 @@ fun Modifier.pagerTabIndicatorOffset( pagerState: PagerState, tabPositions: List, pageIndexMapping: (Int) -> Int = { it }, +): Modifier { + val stateBridge = object : PagerStateBridge { + override val currentPage: Int + get() = pagerState.currentPage + override val currentPageOffset: Float + get() = pagerState.currentPageOffset + } + + return pagerTabIndicatorOffset(stateBridge, tabPositions, pageIndexMapping) +} + +/** + * This indicator syncs up a [TabRow] or [ScrollableTabRow] tab indicator with a + * [androidx.compose.foundation.pager.HorizontalPager] or + * [androidx.compose.foundation.pager.VerticalPager]. + */ +@OptIn(ExperimentalFoundationApi::class) +@ExperimentalPagerApi +fun Modifier.pagerTabIndicatorOffset( + pagerState: androidx.compose.foundation.pager.PagerState, + tabPositions: List, + pageIndexMapping: (Int) -> Int = { it }, +): Modifier { + val stateBridge = object : PagerStateBridge { + override val currentPage: Int + get() = pagerState.currentPage + override val currentPageOffset: Float + get() = pagerState.currentPageOffsetFraction + } + + return pagerTabIndicatorOffset(stateBridge, tabPositions, pageIndexMapping) +} + +private fun Modifier.pagerTabIndicatorOffset( + pagerState: PagerStateBridge, + tabPositions: List, + pageIndexMapping: (Int) -> Int = { it }, ): Modifier = layout { measurable, constraints -> if (tabPositions.isEmpty()) { // If there are no pages, nothing to show @@ -75,3 +112,8 @@ fun Modifier.pagerTabIndicatorOffset( } } } + +internal interface PagerStateBridge { + val currentPage: Int + val currentPageOffset: Float +} From 41a154bc08f7f9d4881b2cb536a7e7e1934cfb41 Mon Sep 17 00:00:00 2001 From: Faithful Uchenna Okoye Date: Thu, 26 Jan 2023 17:32:37 +0000 Subject: [PATCH 007/110] Deprecating FlowRow and FlowColumn in Accompanist for the official FlowRow and FlowColumn in Androidx.Compose Foundations. FlowLayouts is now supported in Androidx.Compose. Provided is a migration guide to move to the official version. --- docs/flowlayout.md | 113 +++++++++++++++++- flowlayout/api/current.api | 32 ++--- .../com/google/accompanist/flowlayout/Flow.kt | 52 ++++++++ .../accompanist/flowlayout/FlowLayoutTest.kt | 1 + .../sample/flowlayout/FlowColumnSample.kt | 1 + .../sample/flowlayout/FlowRowSample.kt | 1 + 6 files changed, 182 insertions(+), 18 deletions(-) diff --git a/docs/flowlayout.md b/docs/flowlayout.md index 767c248ea..bb342a072 100644 --- a/docs/flowlayout.md +++ b/docs/flowlayout.md @@ -2,9 +2,14 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-flowlayout)](https://search.maven.org/search?q=g:com.google.accompanist) -Flow layouts adapted from the versions which were available in [Jetpack Compose][compose] until they were removed. +Flow Layouts in Accompanist is now deprecated. Please see the migration guide below to begin using +Flow Layouts in Androidx. -Unlike the standard `Row` and `Column` composables, these layout children across multiple rows/columns if they exceed the available space. +The official `androidx.compose.foundation` FlowLayouts support is very similar to accompanist/flowlayouts, with a few changes. + +It is most similar to `Row` and `Column` and shares similar modifiers and the scopes. +Unlike the standard `Row` and `Column` composables, these layout children across multiple +rows/columns if they exceed the available space. ## Usage @@ -18,6 +23,110 @@ FlowColumn { } ``` +## Migration Guide to the official FlowLayouts + +1. Replace import packages to point to Androidx.Compose +``` kotlin +import androidx.compose.foundation.layout.FlowColumn +``` + +``` kotlin +import androidx.compose.foundation.layout.FlowRow +``` + +For `FlowColumn`: +2. Replace Modifier `mainAxisAlignment` with `verticalArrangement` +3. Replace Modifier `crossAxisAlignment` with `horizontalAlignment` + +For `FlowRow` +4. `mainAxisAlignment` is now `horizontalArrangement` +5. `crossAxisAlignment` is now `verticalAlignment` + +``` kotlin +FlowColumn( + modifier = Modifier, + verticalArrangement = Arrangement.Top, + horizontalAlignment: = Alignment.Start, + content = { // columns } +) +``` + +``` kotlin +FlowRpw( + modifier = Modifier, + horizontalArrangement = Arrangement.Start, + verticalAlignment: = Alignment.Top, + content = { // rows } +) +``` + +6. Replace `mainAxisSpacing` with `VerticalArrangement.spacedBy(50.dp)` in `FlowColumn` and `HorizontalArrangement.spacedBy(50.dp)` in `FlowRow` +``` kotlin +FlowColumn( + verticalArrangement = VerticalArrangement.spacedBy(50.dp), + content = { // columns } +) +``` + +``` kotlin +FlowRow( + horizontalArrangement = HorizontalArrangement.spacedBy(50.dp), + content = { // rows } +) +``` + +7. `crossAxisSpacing` can be supported by adding a padding to each child + +``` kotlin +FlowRow( + horizontalArrangement = HorizontalArrangement.spacedBy(50.dp), + { Box(Modifier.padding(bottom = 20.dp) } +) +``` + +8. `lastLineMainAxisAlignment` can be supported by using `alignBy` on the respective child + +``` kotlin +FlowRow( + horizontalArrangement = HorizontalArrangement.spacedBy(50.dp) +) { Box(Modifier.alignBy(FirstBaseLine) } + +``` + +### New Features: +#### Add weights to each child +To scale an item based on the size of its parent and the space available, adding weights are perfect. +Adding a weight in `FlowRow` and `FlowColumn` is different than in `Row` and `Column` + +In `FlowLayout` it is based on the number of items placed on the row it falls on and their weights. +First we check to see if an item can fit in the current row or column based on its intrinsic size. +If it fits and has a weight, its final size is grown based on the available space and the number of items +with weights placed on the row or column it falls on. + +Because of the nature of `FlowLayouts` an item only grows and does not reduce in size. Its width in `FlowRow` +or height in `FlowColumn` determines it minimum width or height, and then grows based on its weight +and its available space, and the other items that fall on its row and column and their respective weights. + +If it cannot fit based on its intrinsic minimum size, then it is placed in the next row and column. +Once all the number of items that can fit the new row and column is calculated, +then its final width and size is calculated + +``` kotlin +FlowRow() + { repeat(20) { Box(Modifier.size(20.dp).weight(1f, true) } } + +``` + +#### Create a maximum number of items in row or column +You may choose to limit the number of items that appear in each row in `FlowRow` or column in `FlowColumn` +This can be configured using `maxItemsInEachRow` or `maxItemsInEachColumn`: +``` kotlin +FlowRow(maxItemsInEachRow = 3) + { repeat(10) { Box(Modifier.size(20.dp).weight(1f, true) } } +``` + +## Examples + For examples, refer to the [samples](https://github.com/google/accompanist/tree/main/sample/src/main/java/com/google/accompanist/sample/flowlayout). ## Download diff --git a/flowlayout/api/current.api b/flowlayout/api/current.api index 57c716548..0fee2de0c 100644 --- a/flowlayout/api/current.api +++ b/flowlayout/api/current.api @@ -1,29 +1,29 @@ // Signature format: 4.0 package com.google.accompanist.flowlayout { - public enum FlowCrossAxisAlignment { - enum_constant public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Center; - enum_constant public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment End; - enum_constant public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Start; + @Deprecated public enum FlowCrossAxisAlignment { + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Center; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment End; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Start; } public final class FlowKt { - method @androidx.compose.runtime.Composable public static void FlowColumn(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); - method @androidx.compose.runtime.Composable public static void FlowRow(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); + method @Deprecated @androidx.compose.runtime.Composable public static void FlowColumn(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); + method @Deprecated @androidx.compose.runtime.Composable public static void FlowRow(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); } - public enum MainAxisAlignment { - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment Center; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment End; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceAround; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceBetween; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceEvenly; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment Start; + @Deprecated public enum MainAxisAlignment { + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment Center; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment End; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceAround; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceBetween; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceEvenly; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment Start; } - public enum SizeMode { - enum_constant public static final com.google.accompanist.flowlayout.SizeMode Expand; - enum_constant public static final com.google.accompanist.flowlayout.SizeMode Wrap; + @Deprecated public enum SizeMode { + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.SizeMode Expand; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.SizeMode Wrap; } } diff --git a/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt b/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt index 1ffd98a4d..f68bf2b94 100644 --- a/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt +++ b/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt @@ -44,6 +44,17 @@ import kotlin.math.max * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last row. */ @Composable +@Deprecated( + """ +accompanist/FlowRow is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""", + replaceWith = ReplaceWith( + "FlowRow", + "androidx.compose.foundation.layout.FlowRow", + "androidx.compose.ui.Modifier" + ) +) public fun FlowRow( modifier: Modifier = Modifier, mainAxisSize: SizeMode = SizeMode.Wrap, @@ -82,6 +93,17 @@ public fun FlowRow( * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last column. */ @Composable +@Deprecated( + """ +accompanist/FlowColumn is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""", + replaceWith = ReplaceWith( + "FlowColumn", + "androidx.compose.foundation.layout.FlowColumn", + "androidx.compose.ui.Modifier" + ) +) public fun FlowColumn( modifier: Modifier = Modifier, mainAxisSize: SizeMode = SizeMode.Wrap, @@ -108,6 +130,12 @@ public fun FlowColumn( /** * Used to specify the alignment of a layout's children, in cross axis direction. */ +@Deprecated( + """ +accompanist/FlowCrossAxisAlignment is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public enum class FlowCrossAxisAlignment { /** * Place children such that their center is in the middle of the cross axis. @@ -123,12 +151,24 @@ public enum class FlowCrossAxisAlignment { End, } +@Deprecated( + """ +accompanist/FlowMainAxisAlignment is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public typealias FlowMainAxisAlignment = MainAxisAlignment /** * Layout model that arranges its children in a horizontal or vertical flow. */ @Composable +@Deprecated( + """ +accompanist/Flow is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) private fun Flow( modifier: Modifier, orientation: LayoutOrientation, @@ -278,6 +318,12 @@ private fun Flow( * Used to specify how a layout chooses its own size when multiple behaviors are possible. */ // TODO(popam): remove this when Flow is reworked +@Deprecated( + """ +accompanist/SizeMode is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public enum class SizeMode { /** * Minimize the amount of free space by wrapping the children, @@ -294,6 +340,12 @@ public enum class SizeMode { /** * Used to specify the alignment of a layout's children, in main axis direction. */ +@Deprecated( + """ +accompanist/MainAxisAlignment is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public enum class MainAxisAlignment(internal val arrangement: Arrangement.Vertical) { // TODO(soboleva) support RTl in Flow // workaround for now - use Arrangement that equals to previous Arrangement diff --git a/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt b/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt index 784d1da60..970db01dd 100644 --- a/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt +++ b/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt @@ -33,6 +33,7 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.math.roundToInt +@Suppress("DEPRECATION") @RunWith(AndroidJUnit4::class) class FlowLayoutTest : LayoutTest() { @Test diff --git a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt index 874f2755c..c9b9d305a 100644 --- a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt @@ -29,6 +29,7 @@ import com.google.accompanist.flowlayout.FlowColumn import com.google.accompanist.sample.AccompanistSampleTheme import com.google.accompanist.sample.R +@Suppress("DEPRECATION") class FlowColumnSample : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt index ef5d44b46..3eb06e71a 100644 --- a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt @@ -29,6 +29,7 @@ import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.sample.AccompanistSampleTheme import com.google.accompanist.sample.R +@Suppress("DEPRECATION") class FlowRowSample : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) From 722370f128a514003846edc299cfd97f2cde1ce2 Mon Sep 17 00:00:00 2001 From: Faithful Uchenna Okoye Date: Thu, 26 Jan 2023 17:32:37 +0000 Subject: [PATCH 008/110] [FlowLayout] Deprecate FlowRow and FlowColumn This deprecates FlowRow and FlowColumn in Accompanist for the official FlowRow and FlowColumn in Androidx.Compose Foundations. FlowLayouts is now supported in Androidx.Compose. Provided is a migration guide to move to the official version. --- docs/flowlayout.md | 113 +++++++++++++++++- flowlayout/api/current.api | 32 ++--- .../com/google/accompanist/flowlayout/Flow.kt | 52 ++++++++ .../accompanist/flowlayout/FlowLayoutTest.kt | 1 + .../sample/flowlayout/FlowColumnSample.kt | 1 + .../sample/flowlayout/FlowRowSample.kt | 1 + 6 files changed, 182 insertions(+), 18 deletions(-) diff --git a/docs/flowlayout.md b/docs/flowlayout.md index 767c248ea..bb342a072 100644 --- a/docs/flowlayout.md +++ b/docs/flowlayout.md @@ -2,9 +2,14 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-flowlayout)](https://search.maven.org/search?q=g:com.google.accompanist) -Flow layouts adapted from the versions which were available in [Jetpack Compose][compose] until they were removed. +Flow Layouts in Accompanist is now deprecated. Please see the migration guide below to begin using +Flow Layouts in Androidx. -Unlike the standard `Row` and `Column` composables, these layout children across multiple rows/columns if they exceed the available space. +The official `androidx.compose.foundation` FlowLayouts support is very similar to accompanist/flowlayouts, with a few changes. + +It is most similar to `Row` and `Column` and shares similar modifiers and the scopes. +Unlike the standard `Row` and `Column` composables, these layout children across multiple +rows/columns if they exceed the available space. ## Usage @@ -18,6 +23,110 @@ FlowColumn { } ``` +## Migration Guide to the official FlowLayouts + +1. Replace import packages to point to Androidx.Compose +``` kotlin +import androidx.compose.foundation.layout.FlowColumn +``` + +``` kotlin +import androidx.compose.foundation.layout.FlowRow +``` + +For `FlowColumn`: +2. Replace Modifier `mainAxisAlignment` with `verticalArrangement` +3. Replace Modifier `crossAxisAlignment` with `horizontalAlignment` + +For `FlowRow` +4. `mainAxisAlignment` is now `horizontalArrangement` +5. `crossAxisAlignment` is now `verticalAlignment` + +``` kotlin +FlowColumn( + modifier = Modifier, + verticalArrangement = Arrangement.Top, + horizontalAlignment: = Alignment.Start, + content = { // columns } +) +``` + +``` kotlin +FlowRpw( + modifier = Modifier, + horizontalArrangement = Arrangement.Start, + verticalAlignment: = Alignment.Top, + content = { // rows } +) +``` + +6. Replace `mainAxisSpacing` with `VerticalArrangement.spacedBy(50.dp)` in `FlowColumn` and `HorizontalArrangement.spacedBy(50.dp)` in `FlowRow` +``` kotlin +FlowColumn( + verticalArrangement = VerticalArrangement.spacedBy(50.dp), + content = { // columns } +) +``` + +``` kotlin +FlowRow( + horizontalArrangement = HorizontalArrangement.spacedBy(50.dp), + content = { // rows } +) +``` + +7. `crossAxisSpacing` can be supported by adding a padding to each child + +``` kotlin +FlowRow( + horizontalArrangement = HorizontalArrangement.spacedBy(50.dp), + { Box(Modifier.padding(bottom = 20.dp) } +) +``` + +8. `lastLineMainAxisAlignment` can be supported by using `alignBy` on the respective child + +``` kotlin +FlowRow( + horizontalArrangement = HorizontalArrangement.spacedBy(50.dp) +) { Box(Modifier.alignBy(FirstBaseLine) } + +``` + +### New Features: +#### Add weights to each child +To scale an item based on the size of its parent and the space available, adding weights are perfect. +Adding a weight in `FlowRow` and `FlowColumn` is different than in `Row` and `Column` + +In `FlowLayout` it is based on the number of items placed on the row it falls on and their weights. +First we check to see if an item can fit in the current row or column based on its intrinsic size. +If it fits and has a weight, its final size is grown based on the available space and the number of items +with weights placed on the row or column it falls on. + +Because of the nature of `FlowLayouts` an item only grows and does not reduce in size. Its width in `FlowRow` +or height in `FlowColumn` determines it minimum width or height, and then grows based on its weight +and its available space, and the other items that fall on its row and column and their respective weights. + +If it cannot fit based on its intrinsic minimum size, then it is placed in the next row and column. +Once all the number of items that can fit the new row and column is calculated, +then its final width and size is calculated + +``` kotlin +FlowRow() + { repeat(20) { Box(Modifier.size(20.dp).weight(1f, true) } } + +``` + +#### Create a maximum number of items in row or column +You may choose to limit the number of items that appear in each row in `FlowRow` or column in `FlowColumn` +This can be configured using `maxItemsInEachRow` or `maxItemsInEachColumn`: +``` kotlin +FlowRow(maxItemsInEachRow = 3) + { repeat(10) { Box(Modifier.size(20.dp).weight(1f, true) } } +``` + +## Examples + For examples, refer to the [samples](https://github.com/google/accompanist/tree/main/sample/src/main/java/com/google/accompanist/sample/flowlayout). ## Download diff --git a/flowlayout/api/current.api b/flowlayout/api/current.api index 57c716548..0fee2de0c 100644 --- a/flowlayout/api/current.api +++ b/flowlayout/api/current.api @@ -1,29 +1,29 @@ // Signature format: 4.0 package com.google.accompanist.flowlayout { - public enum FlowCrossAxisAlignment { - enum_constant public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Center; - enum_constant public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment End; - enum_constant public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Start; + @Deprecated public enum FlowCrossAxisAlignment { + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Center; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment End; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Start; } public final class FlowKt { - method @androidx.compose.runtime.Composable public static void FlowColumn(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); - method @androidx.compose.runtime.Composable public static void FlowRow(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); + method @Deprecated @androidx.compose.runtime.Composable public static void FlowColumn(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); + method @Deprecated @androidx.compose.runtime.Composable public static void FlowRow(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); } - public enum MainAxisAlignment { - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment Center; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment End; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceAround; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceBetween; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceEvenly; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment Start; + @Deprecated public enum MainAxisAlignment { + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment Center; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment End; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceAround; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceBetween; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceEvenly; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment Start; } - public enum SizeMode { - enum_constant public static final com.google.accompanist.flowlayout.SizeMode Expand; - enum_constant public static final com.google.accompanist.flowlayout.SizeMode Wrap; + @Deprecated public enum SizeMode { + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.SizeMode Expand; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.SizeMode Wrap; } } diff --git a/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt b/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt index 1ffd98a4d..f68bf2b94 100644 --- a/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt +++ b/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt @@ -44,6 +44,17 @@ import kotlin.math.max * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last row. */ @Composable +@Deprecated( + """ +accompanist/FlowRow is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""", + replaceWith = ReplaceWith( + "FlowRow", + "androidx.compose.foundation.layout.FlowRow", + "androidx.compose.ui.Modifier" + ) +) public fun FlowRow( modifier: Modifier = Modifier, mainAxisSize: SizeMode = SizeMode.Wrap, @@ -82,6 +93,17 @@ public fun FlowRow( * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last column. */ @Composable +@Deprecated( + """ +accompanist/FlowColumn is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""", + replaceWith = ReplaceWith( + "FlowColumn", + "androidx.compose.foundation.layout.FlowColumn", + "androidx.compose.ui.Modifier" + ) +) public fun FlowColumn( modifier: Modifier = Modifier, mainAxisSize: SizeMode = SizeMode.Wrap, @@ -108,6 +130,12 @@ public fun FlowColumn( /** * Used to specify the alignment of a layout's children, in cross axis direction. */ +@Deprecated( + """ +accompanist/FlowCrossAxisAlignment is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public enum class FlowCrossAxisAlignment { /** * Place children such that their center is in the middle of the cross axis. @@ -123,12 +151,24 @@ public enum class FlowCrossAxisAlignment { End, } +@Deprecated( + """ +accompanist/FlowMainAxisAlignment is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public typealias FlowMainAxisAlignment = MainAxisAlignment /** * Layout model that arranges its children in a horizontal or vertical flow. */ @Composable +@Deprecated( + """ +accompanist/Flow is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) private fun Flow( modifier: Modifier, orientation: LayoutOrientation, @@ -278,6 +318,12 @@ private fun Flow( * Used to specify how a layout chooses its own size when multiple behaviors are possible. */ // TODO(popam): remove this when Flow is reworked +@Deprecated( + """ +accompanist/SizeMode is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public enum class SizeMode { /** * Minimize the amount of free space by wrapping the children, @@ -294,6 +340,12 @@ public enum class SizeMode { /** * Used to specify the alignment of a layout's children, in main axis direction. */ +@Deprecated( + """ +accompanist/MainAxisAlignment is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public enum class MainAxisAlignment(internal val arrangement: Arrangement.Vertical) { // TODO(soboleva) support RTl in Flow // workaround for now - use Arrangement that equals to previous Arrangement diff --git a/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt b/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt index 784d1da60..970db01dd 100644 --- a/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt +++ b/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt @@ -33,6 +33,7 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.math.roundToInt +@Suppress("DEPRECATION") @RunWith(AndroidJUnit4::class) class FlowLayoutTest : LayoutTest() { @Test diff --git a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt index 874f2755c..c9b9d305a 100644 --- a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt @@ -29,6 +29,7 @@ import com.google.accompanist.flowlayout.FlowColumn import com.google.accompanist.sample.AccompanistSampleTheme import com.google.accompanist.sample.R +@Suppress("DEPRECATION") class FlowColumnSample : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt index ef5d44b46..3eb06e71a 100644 --- a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt @@ -29,6 +29,7 @@ import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.sample.AccompanistSampleTheme import com.google.accompanist.sample.R +@Suppress("DEPRECATION") class FlowRowSample : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) From d6f7ddf22dda98e21e6e7923fd4a6399de1b1f16 Mon Sep 17 00:00:00 2001 From: Faithful Uchenna Okoye Date: Thu, 26 Jan 2023 17:32:37 +0000 Subject: [PATCH 009/110] [FlowLayout] Deprecate FlowRow and FlowColumn This deprecates FlowRow and FlowColumn in Accompanist for the official FlowRow and FlowColumn in Androidx.Compose Foundations. FlowLayouts is now supported in Androidx.Compose. Provided is a migration guide to move to the official version. --- docs/flowlayout.md | 113 +++++++++++++++++- flowlayout/api/current.api | 32 ++--- .../com/google/accompanist/flowlayout/Flow.kt | 52 ++++++++ .../accompanist/flowlayout/FlowLayoutTest.kt | 1 + .../sample/flowlayout/FlowColumnSample.kt | 1 + .../sample/flowlayout/FlowRowSample.kt | 1 + 6 files changed, 182 insertions(+), 18 deletions(-) diff --git a/docs/flowlayout.md b/docs/flowlayout.md index 767c248ea..bb342a072 100644 --- a/docs/flowlayout.md +++ b/docs/flowlayout.md @@ -2,9 +2,14 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-flowlayout)](https://search.maven.org/search?q=g:com.google.accompanist) -Flow layouts adapted from the versions which were available in [Jetpack Compose][compose] until they were removed. +Flow Layouts in Accompanist is now deprecated. Please see the migration guide below to begin using +Flow Layouts in Androidx. -Unlike the standard `Row` and `Column` composables, these layout children across multiple rows/columns if they exceed the available space. +The official `androidx.compose.foundation` FlowLayouts support is very similar to accompanist/flowlayouts, with a few changes. + +It is most similar to `Row` and `Column` and shares similar modifiers and the scopes. +Unlike the standard `Row` and `Column` composables, these layout children across multiple +rows/columns if they exceed the available space. ## Usage @@ -18,6 +23,110 @@ FlowColumn { } ``` +## Migration Guide to the official FlowLayouts + +1. Replace import packages to point to Androidx.Compose +``` kotlin +import androidx.compose.foundation.layout.FlowColumn +``` + +``` kotlin +import androidx.compose.foundation.layout.FlowRow +``` + +For `FlowColumn`: +2. Replace Modifier `mainAxisAlignment` with `verticalArrangement` +3. Replace Modifier `crossAxisAlignment` with `horizontalAlignment` + +For `FlowRow` +4. `mainAxisAlignment` is now `horizontalArrangement` +5. `crossAxisAlignment` is now `verticalAlignment` + +``` kotlin +FlowColumn( + modifier = Modifier, + verticalArrangement = Arrangement.Top, + horizontalAlignment: = Alignment.Start, + content = { // columns } +) +``` + +``` kotlin +FlowRpw( + modifier = Modifier, + horizontalArrangement = Arrangement.Start, + verticalAlignment: = Alignment.Top, + content = { // rows } +) +``` + +6. Replace `mainAxisSpacing` with `VerticalArrangement.spacedBy(50.dp)` in `FlowColumn` and `HorizontalArrangement.spacedBy(50.dp)` in `FlowRow` +``` kotlin +FlowColumn( + verticalArrangement = VerticalArrangement.spacedBy(50.dp), + content = { // columns } +) +``` + +``` kotlin +FlowRow( + horizontalArrangement = HorizontalArrangement.spacedBy(50.dp), + content = { // rows } +) +``` + +7. `crossAxisSpacing` can be supported by adding a padding to each child + +``` kotlin +FlowRow( + horizontalArrangement = HorizontalArrangement.spacedBy(50.dp), + { Box(Modifier.padding(bottom = 20.dp) } +) +``` + +8. `lastLineMainAxisAlignment` can be supported by using `alignBy` on the respective child + +``` kotlin +FlowRow( + horizontalArrangement = HorizontalArrangement.spacedBy(50.dp) +) { Box(Modifier.alignBy(FirstBaseLine) } + +``` + +### New Features: +#### Add weights to each child +To scale an item based on the size of its parent and the space available, adding weights are perfect. +Adding a weight in `FlowRow` and `FlowColumn` is different than in `Row` and `Column` + +In `FlowLayout` it is based on the number of items placed on the row it falls on and their weights. +First we check to see if an item can fit in the current row or column based on its intrinsic size. +If it fits and has a weight, its final size is grown based on the available space and the number of items +with weights placed on the row or column it falls on. + +Because of the nature of `FlowLayouts` an item only grows and does not reduce in size. Its width in `FlowRow` +or height in `FlowColumn` determines it minimum width or height, and then grows based on its weight +and its available space, and the other items that fall on its row and column and their respective weights. + +If it cannot fit based on its intrinsic minimum size, then it is placed in the next row and column. +Once all the number of items that can fit the new row and column is calculated, +then its final width and size is calculated + +``` kotlin +FlowRow() + { repeat(20) { Box(Modifier.size(20.dp).weight(1f, true) } } + +``` + +#### Create a maximum number of items in row or column +You may choose to limit the number of items that appear in each row in `FlowRow` or column in `FlowColumn` +This can be configured using `maxItemsInEachRow` or `maxItemsInEachColumn`: +``` kotlin +FlowRow(maxItemsInEachRow = 3) + { repeat(10) { Box(Modifier.size(20.dp).weight(1f, true) } } +``` + +## Examples + For examples, refer to the [samples](https://github.com/google/accompanist/tree/main/sample/src/main/java/com/google/accompanist/sample/flowlayout). ## Download diff --git a/flowlayout/api/current.api b/flowlayout/api/current.api index 57c716548..0fee2de0c 100644 --- a/flowlayout/api/current.api +++ b/flowlayout/api/current.api @@ -1,29 +1,29 @@ // Signature format: 4.0 package com.google.accompanist.flowlayout { - public enum FlowCrossAxisAlignment { - enum_constant public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Center; - enum_constant public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment End; - enum_constant public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Start; + @Deprecated public enum FlowCrossAxisAlignment { + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Center; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment End; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.FlowCrossAxisAlignment Start; } public final class FlowKt { - method @androidx.compose.runtime.Composable public static void FlowColumn(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); - method @androidx.compose.runtime.Composable public static void FlowRow(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); + method @Deprecated @androidx.compose.runtime.Composable public static void FlowColumn(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); + method @Deprecated @androidx.compose.runtime.Composable public static void FlowRow(optional androidx.compose.ui.Modifier modifier, optional com.google.accompanist.flowlayout.SizeMode mainAxisSize, optional com.google.accompanist.flowlayout.MainAxisAlignment mainAxisAlignment, optional float mainAxisSpacing, optional com.google.accompanist.flowlayout.FlowCrossAxisAlignment crossAxisAlignment, optional float crossAxisSpacing, optional com.google.accompanist.flowlayout.MainAxisAlignment lastLineMainAxisAlignment, kotlin.jvm.functions.Function0 content); } - public enum MainAxisAlignment { - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment Center; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment End; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceAround; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceBetween; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceEvenly; - enum_constant public static final com.google.accompanist.flowlayout.MainAxisAlignment Start; + @Deprecated public enum MainAxisAlignment { + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment Center; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment End; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceAround; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceBetween; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment SpaceEvenly; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.MainAxisAlignment Start; } - public enum SizeMode { - enum_constant public static final com.google.accompanist.flowlayout.SizeMode Expand; - enum_constant public static final com.google.accompanist.flowlayout.SizeMode Wrap; + @Deprecated public enum SizeMode { + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.SizeMode Expand; + enum_constant @Deprecated public static final com.google.accompanist.flowlayout.SizeMode Wrap; } } diff --git a/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt b/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt index 1ffd98a4d..f68bf2b94 100644 --- a/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt +++ b/flowlayout/src/main/java/com/google/accompanist/flowlayout/Flow.kt @@ -44,6 +44,17 @@ import kotlin.math.max * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last row. */ @Composable +@Deprecated( + """ +accompanist/FlowRow is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""", + replaceWith = ReplaceWith( + "FlowRow", + "androidx.compose.foundation.layout.FlowRow", + "androidx.compose.ui.Modifier" + ) +) public fun FlowRow( modifier: Modifier = Modifier, mainAxisSize: SizeMode = SizeMode.Wrap, @@ -82,6 +93,17 @@ public fun FlowRow( * @param lastLineMainAxisAlignment Overrides the main axis alignment of the last column. */ @Composable +@Deprecated( + """ +accompanist/FlowColumn is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""", + replaceWith = ReplaceWith( + "FlowColumn", + "androidx.compose.foundation.layout.FlowColumn", + "androidx.compose.ui.Modifier" + ) +) public fun FlowColumn( modifier: Modifier = Modifier, mainAxisSize: SizeMode = SizeMode.Wrap, @@ -108,6 +130,12 @@ public fun FlowColumn( /** * Used to specify the alignment of a layout's children, in cross axis direction. */ +@Deprecated( + """ +accompanist/FlowCrossAxisAlignment is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public enum class FlowCrossAxisAlignment { /** * Place children such that their center is in the middle of the cross axis. @@ -123,12 +151,24 @@ public enum class FlowCrossAxisAlignment { End, } +@Deprecated( + """ +accompanist/FlowMainAxisAlignment is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public typealias FlowMainAxisAlignment = MainAxisAlignment /** * Layout model that arranges its children in a horizontal or vertical flow. */ @Composable +@Deprecated( + """ +accompanist/Flow is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) private fun Flow( modifier: Modifier, orientation: LayoutOrientation, @@ -278,6 +318,12 @@ private fun Flow( * Used to specify how a layout chooses its own size when multiple behaviors are possible. */ // TODO(popam): remove this when Flow is reworked +@Deprecated( + """ +accompanist/SizeMode is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public enum class SizeMode { /** * Minimize the amount of free space by wrapping the children, @@ -294,6 +340,12 @@ public enum class SizeMode { /** * Used to specify the alignment of a layout's children, in main axis direction. */ +@Deprecated( + """ +accompanist/MainAxisAlignment is deprecated. +For more migration information, please visit https://google.github.io/accompanist/flowlayouts/#migration +""" +) public enum class MainAxisAlignment(internal val arrangement: Arrangement.Vertical) { // TODO(soboleva) support RTl in Flow // workaround for now - use Arrangement that equals to previous Arrangement diff --git a/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt b/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt index 784d1da60..970db01dd 100644 --- a/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt +++ b/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/FlowLayoutTest.kt @@ -33,6 +33,7 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.math.roundToInt +@Suppress("DEPRECATION") @RunWith(AndroidJUnit4::class) class FlowLayoutTest : LayoutTest() { @Test diff --git a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt index 874f2755c..c9b9d305a 100644 --- a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowColumnSample.kt @@ -29,6 +29,7 @@ import com.google.accompanist.flowlayout.FlowColumn import com.google.accompanist.sample.AccompanistSampleTheme import com.google.accompanist.sample.R +@Suppress("DEPRECATION") class FlowColumnSample : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt index ef5d44b46..3eb06e71a 100644 --- a/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/flowlayout/FlowRowSample.kt @@ -29,6 +29,7 @@ import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.sample.AccompanistSampleTheme import com.google.accompanist.sample.R +@Suppress("DEPRECATION") class FlowRowSample : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) From 0f3c0bf7bd60b4643e54d3942a1d6ffb2dacbf1c Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Fri, 27 Jan 2023 08:13:58 +1100 Subject: [PATCH 010/110] Upgrade to Compose 1.4-alpha05 --- gradle.properties | 2 +- gradle/libs.versions.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 61a9e46f3..c2a5859da 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,7 +33,7 @@ systemProp.org.gradle.internal.http.socketTimeout=120000 GROUP=com.google.accompanist # !! No longer need to update this manually when using a Compose SNAPSHOT -VERSION_NAME=0.29.1-SNAPSHOT +VERSION_NAME=0.29.1-alpha POM_DESCRIPTION=Utilities for Jetpack Compose diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dbfb3c880..55d6c4f31 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -compose = "1.4.0-alpha04" -composeCompiler = "1.4.0-alpha02" +compose = "1.4.0-alpha05" +composeCompiler = "1.4.0" composeMaterial3 = "1.0.1" composesnapshot = "-" # a single character = no snapshot @@ -10,7 +10,7 @@ gradlePlugin = "7.3.1" lintMinCompose = "30.0.0" ktlint = "0.45.2" -kotlin = "1.7.21" +kotlin = "1.8.0" coroutines = "1.6.4" okhttp = "3.12.13" coil = "1.3.2" From bed56ead9bc18b71e60d6a8746923c87b1b95015 Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Fri, 27 Jan 2023 08:18:46 +1100 Subject: [PATCH 011/110] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c311fe701..baafe082a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ A library that enables the reuse of [MDC-Android][mdc] Material 2 XML themes, fo ### 🎨 [Material 3 Theme Adapter](./themeadapter-material3/) A library that enables the reuse of [MDC-Android][mdc] Material 3 XML themes, for theming in Jetpack Compose. -### 📖 [Pager](./pager/) +### 📖 [Pager](./pager/) (Soon to be deprecated - Upstreamed in Compose 1.4) A library that provides utilities for building paginated layouts in Jetpack Compose, similar to Android's [ViewPager][viewpager]. ### 📫 [Permissions](./permissions/) @@ -55,7 +55,7 @@ A library that provides [Android runtime permissions][runtimepermissions] suppor ### ⏳ [Placeholder](./placeholder/) A library that provides easy-to-use modifiers for displaying a placeholder UI while content is loading. -### 🌊 [Flow Layouts](./flowlayout/) +### 🌊 [Flow Layouts](./flowlayout/) (Soon to be deprecated - Upstreamed in Compose 1.4) A library that adds Flexbox-like layout components to Jetpack Compose. ### 🧭✨[Navigation-Animation](./navigation-animation/) From 154a7a6e74e92997bbc6875692e5f3a1ca8eba2b Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Mon, 30 Jan 2023 08:07:51 +1100 Subject: [PATCH 012/110] Suppress NewApi warning for forEach --- .../navigation/animation/AnimatedComposeNavigator.kt | 3 +++ .../google/accompanist/navigation/animation/NavGraphBuilder.kt | 2 ++ 2 files changed, 5 insertions(+) diff --git a/navigation-animation/src/main/java/com/google/accompanist/navigation/animation/AnimatedComposeNavigator.kt b/navigation-animation/src/main/java/com/google/accompanist/navigation/animation/AnimatedComposeNavigator.kt index effa9dc70..4425bba4c 100644 --- a/navigation-animation/src/main/java/com/google/accompanist/navigation/animation/AnimatedComposeNavigator.kt +++ b/navigation-animation/src/main/java/com/google/accompanist/navigation/animation/AnimatedComposeNavigator.kt @@ -16,6 +16,7 @@ package com.google.accompanist.navigation.animation +import android.annotation.SuppressLint import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.Composable @@ -24,6 +25,7 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination import androidx.navigation.NavOptions import androidx.navigation.Navigator +import kotlin.collections.forEach /** * Navigator that navigates through [Composable]s. Every destination using this Navigator must @@ -39,6 +41,7 @@ public class AnimatedComposeNavigator : Navigator, navOptions: NavOptions?, diff --git a/navigation-animation/src/main/java/com/google/accompanist/navigation/animation/NavGraphBuilder.kt b/navigation-animation/src/main/java/com/google/accompanist/navigation/animation/NavGraphBuilder.kt index 887388efe..dce5cd2b2 100644 --- a/navigation-animation/src/main/java/com/google/accompanist/navigation/animation/NavGraphBuilder.kt +++ b/navigation-animation/src/main/java/com/google/accompanist/navigation/animation/NavGraphBuilder.kt @@ -16,6 +16,7 @@ package com.google.accompanist.navigation.animation +import android.annotation.SuppressLint import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.EnterTransition @@ -42,6 +43,7 @@ import androidx.navigation.get * @param popExitTransition callback to determine the destination's popExit transition * @param content composable for the destination */ +@SuppressLint("NewApi") // b/187418647 @ExperimentalAnimationApi public fun NavGraphBuilder.composable( route: String, From 9e1e305b4743fdcf46a4dad4f04c137c986d2554 Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Mon, 30 Jan 2023 09:15:50 +1100 Subject: [PATCH 013/110] Fix lint errors --- adaptive/build.gradle | 2 +- .../kotlin/com/google/accompanist/flowlayout/LayoutTest.kt | 2 +- .../accompanist/navigation/material/BottomSheetNavigator.kt | 2 ++ .../google/accompanist/navigation/material/NavGraphBuilder.kt | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/adaptive/build.gradle b/adaptive/build.gradle index 82dff668b..7052b3a27 100644 --- a/adaptive/build.gradle +++ b/adaptive/build.gradle @@ -32,7 +32,7 @@ android { defaultConfig { minSdkVersion 21 // targetSdkVersion has no effect for libraries. This is only used for the test APK - targetSdkVersion 32 + targetSdkVersion 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/LayoutTest.kt b/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/LayoutTest.kt index 45d979b13..6827e0ec8 100644 --- a/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/LayoutTest.kt +++ b/flowlayout/src/sharedTest/kotlin/com/google/accompanist/flowlayout/LayoutTest.kt @@ -60,7 +60,7 @@ open class LayoutTest { size: Ref, position: Ref, positionedLatch: CountDownLatch - ): Modifier = onGloballyPositioned { coordinates -> + ): Modifier = this then onGloballyPositioned { coordinates -> size.value = IntSize(coordinates.size.width, coordinates.size.height) position.value = coordinates.localToRoot(Offset(0f, 0f)) positionedLatch.countDown() diff --git a/navigation-material/src/main/java/com/google/accompanist/navigation/material/BottomSheetNavigator.kt b/navigation-material/src/main/java/com/google/accompanist/navigation/material/BottomSheetNavigator.kt index 86b3ba4ef..932fc07c5 100644 --- a/navigation-material/src/main/java/com/google/accompanist/navigation/material/BottomSheetNavigator.kt +++ b/navigation-material/src/main/java/com/google/accompanist/navigation/material/BottomSheetNavigator.kt @@ -16,6 +16,7 @@ package com.google.accompanist.navigation.material +import android.annotation.SuppressLint import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.ExperimentalMaterialApi @@ -247,6 +248,7 @@ class BottomSheetNavigator( content = {} ) + @SuppressLint("NewApi") // b/187418647 override fun navigate( entries: List, navOptions: NavOptions?, diff --git a/navigation-material/src/main/java/com/google/accompanist/navigation/material/NavGraphBuilder.kt b/navigation-material/src/main/java/com/google/accompanist/navigation/material/NavGraphBuilder.kt index 21a0dd68a..55d8e205c 100644 --- a/navigation-material/src/main/java/com/google/accompanist/navigation/material/NavGraphBuilder.kt +++ b/navigation-material/src/main/java/com/google/accompanist/navigation/material/NavGraphBuilder.kt @@ -16,6 +16,7 @@ package com.google.accompanist.navigation.material +import android.annotation.SuppressLint import androidx.compose.foundation.layout.ColumnScope import androidx.compose.runtime.Composable import androidx.navigation.NamedNavArgument @@ -32,6 +33,7 @@ import androidx.navigation.get * @param deepLinks list of deep links to associate with the destinations * @param content the sheet content at the given destination */ +@SuppressLint("NewApi") // b/187418647 @ExperimentalMaterialNavigationApi public fun NavGraphBuilder.bottomSheet( route: String, From ced01228f45e00dee00dbafdd90326b016666687 Mon Sep 17 00:00:00 2001 From: Ben Trengrove Date: Mon, 30 Jan 2023 09:45:31 +1100 Subject: [PATCH 014/110] Fix modifier lint warnings --- .idea/inspectionProfiles/ktlint.xml | 29 ++++++++++++++++++- .idea/kotlinc.xml | 2 +- .../com/google/accompanist/insets/Padding.kt | 14 ++++----- .../com/google/accompanist/insets/Size.kt | 6 ++-- .../com/google/accompanist/pager/PagerTab.kt | 2 +- .../placeholder/material/Placeholder.kt | 2 +- .../placeholder/material3/Placeholder.kt | 2 +- .../accompanist/placeholder/Placeholder.kt | 2 +- .../google/accompanist/sample/MainActivity.kt | 1 + 9 files changed, 44 insertions(+), 16 deletions(-) diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml index 7d04a74be..677073a24 100644 --- a/.idea/inspectionProfiles/ktlint.xml +++ b/.idea/inspectionProfiles/ktlint.xml @@ -2,6 +2,33 @@ - + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index dd185e22b..22dcb880f 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -4,6 +4,6 @@