From e888bde3f46b9ece23daa095ac2b9352d50c2e08 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Thu, 29 Oct 2020 15:04:27 +0000 Subject: [PATCH 01/22] Add Insetter skeleton project --- insetter/README.md | 19 ++++ insetter/api/insetter.api | 0 insetter/build.gradle | 93 +++++++++++++++++++ insetter/gradle.properties | 3 + insetter/src/androidTest/AndroidManifest.xml | 22 +++++ .../accompanist/insetter/InsetterTest.kt | 32 +++++++ insetter/src/main/AndroidManifest.xml | 18 ++++ settings.gradle | 1 + 8 files changed, 188 insertions(+) create mode 100644 insetter/README.md create mode 100644 insetter/api/insetter.api create mode 100644 insetter/build.gradle create mode 100644 insetter/gradle.properties create mode 100644 insetter/src/androidTest/AndroidManifest.xml create mode 100644 insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt create mode 100644 insetter/src/main/AndroidManifest.xml diff --git a/insetter/README.md b/insetter/README.md new file mode 100644 index 000000000..08effd352 --- /dev/null +++ b/insetter/README.md @@ -0,0 +1,19 @@ +# Jetpack Compose + Insetter + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.chrisbanes.accompanist/accompanist-insetter/badge.svg)](https://search.maven.org/search?q=g:dev.chrisbanes.accompanist) + +TODO + +## Download + +```groovy +repositories { + mavenCentral() +} + +dependencies { + implementation "dev.chrisbanes.accompanist:accompanist-insetter:" +} +``` + +Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit. diff --git a/insetter/api/insetter.api b/insetter/api/insetter.api new file mode 100644 index 000000000..e69de29bb diff --git a/insetter/build.gradle b/insetter/build.gradle new file mode 100644 index 000000000..86ee7f8a7 --- /dev/null +++ b/insetter/build.gradle @@ -0,0 +1,93 @@ +/* + * Copyright 2020 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. + */ + +import dev.chrisbanes.accompanist.buildsrc.Libs + +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'org.jetbrains.dokka' +} + +kotlin { + explicitApi() +} + +android { + compileSdkVersion 30 + + defaultConfig { + minSdkVersion 21 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildFeatures { + buildConfig false + compose true + } + + composeOptions { + kotlinCompilerVersion Libs.Kotlin.version + kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version + } + + lintOptions { + textReport true + textOutput 'stdout' + // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks + checkReleaseBuilds false + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +afterEvaluate { + tasks.withType(org.jetbrains.dokka.gradle.DokkaTask).configureEach { + outputDirectory.set(rootProject.file('docs/api')) + } +} + +dependencies { + implementation Libs.AndroidX.coreKtx + implementation Libs.AndroidX.Compose.runtime + implementation Libs.AndroidX.Compose.foundation + + implementation Libs.Kotlin.stdlib + // implementation Libs.Coroutines.android + + // ====================== + // Test dependencies + // ====================== + + androidTestImplementation Libs.junit + androidTestImplementation Libs.truth + + androidTestImplementation Libs.AndroidX.Compose.test + androidTestImplementation Libs.AndroidX.Compose.ui + androidTestImplementation Libs.AndroidX.Test.rules + androidTestImplementation Libs.AndroidX.Test.runner +} + +apply plugin: "com.vanniktech.maven.publish" diff --git a/insetter/gradle.properties b/insetter/gradle.properties new file mode 100644 index 000000000..ecd37ad0a --- /dev/null +++ b/insetter/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=accompanist-insetter +POM_NAME=Accompanist Insetter library +POM_PACKAGING=aar \ No newline at end of file diff --git a/insetter/src/androidTest/AndroidManifest.xml b/insetter/src/androidTest/AndroidManifest.xml new file mode 100644 index 000000000..c392b2f51 --- /dev/null +++ b/insetter/src/androidTest/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt b/insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt new file mode 100644 index 000000000..bd30b0de1 --- /dev/null +++ b/insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 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 dev.chrisbanes.accompanist.insetter + +import androidx.test.filters.LargeTest +import androidx.ui.test.createComposeRule +import org.junit.Rule +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@LargeTest +@RunWith(JUnit4::class) +class InsetterTest { + @get:Rule + val composeTestRule = createComposeRule() + + +} diff --git a/insetter/src/main/AndroidManifest.xml b/insetter/src/main/AndroidManifest.xml new file mode 100644 index 000000000..4b992b501 --- /dev/null +++ b/insetter/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/settings.gradle b/settings.gradle index fecc699ac..cb55bd5a5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -30,4 +30,5 @@ include ':picasso' include ':glide' include ':imageloading-core' include ':imageloading-testutils' +include ':insetter' include ':sample' From 5f262b1d85f187b972bc3aef5428818a0cb70bfb Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Thu, 29 Oct 2020 15:26:20 +0000 Subject: [PATCH 02/22] Import goo.gle/compose-insets --- .../accompanist/buildsrc/dependencies.kt | 2 +- insetter/api/insetter.api | 51 ++ insetter/build.gradle | 2 +- .../accompanist/insetter/InsetterTest.kt | 2 - .../accompanist/insetter/Insetter.kt | 608 ++++++++++++++++++ sample/build.gradle | 4 - 6 files changed, 661 insertions(+), 8 deletions(-) create mode 100644 insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt diff --git a/buildSrc/src/main/java/dev/chrisbanes/accompanist/buildsrc/dependencies.kt b/buildSrc/src/main/java/dev/chrisbanes/accompanist/buildsrc/dependencies.kt index 65debdba1..7b51f6b30 100644 --- a/buildSrc/src/main/java/dev/chrisbanes/accompanist/buildsrc/dependencies.kt +++ b/buildSrc/src/main/java/dev/chrisbanes/accompanist/buildsrc/dependencies.kt @@ -85,7 +85,7 @@ object Libs { const val core = "androidx.core:core:1.2.0" const val coreKtx = "androidx.core:core-ktx:1.2.0" - const val appcompat = "androidx.appcompat:appcompat:1.3.0-alpha02" + const val coreAlpha = "androidx.core:core:1.5.0-alpha04" } object Coil { diff --git a/insetter/api/insetter.api b/insetter/api/insetter.api index e69de29bb..dbb921d01 100644 --- a/insetter/api/insetter.api +++ b/insetter/api/insetter.api @@ -0,0 +1,51 @@ +public final class dev/chrisbanes/accompanist/insetter/DisplayInsets { + public fun ()V + public final fun getIme ()Ldev/chrisbanes/accompanist/insetter/Insets; + public final fun getNavigationBars ()Ldev/chrisbanes/accompanist/insetter/Insets; + public final fun getStatusBars ()Ldev/chrisbanes/accompanist/insetter/Insets; + public final fun getSystemBars ()Ldev/chrisbanes/accompanist/insetter/Insets; + public final fun getSystemGestures ()Ldev/chrisbanes/accompanist/insetter/Insets; +} + +public final class dev/chrisbanes/accompanist/insetter/HorizontalSide : java/lang/Enum { + public static final field Left Ldev/chrisbanes/accompanist/insetter/HorizontalSide; + public static final field Right Ldev/chrisbanes/accompanist/insetter/HorizontalSide; + public static final fun valueOf (Ljava/lang/String;)Ldev/chrisbanes/accompanist/insetter/HorizontalSide; + public static final fun values ()[Ldev/chrisbanes/accompanist/insetter/HorizontalSide; +} + +public final class dev/chrisbanes/accompanist/insetter/Insets { + public fun ()V + public final fun getBottom ()I + public final fun getLeft ()I + public final fun getRight ()I + public final fun getTop ()I + public final fun isVisible ()Z +} + +public final class dev/chrisbanes/accompanist/insetter/InsetterKt { + public static final fun ProvideDisplayInsets (ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun getAmbientInsets ()Landroidx/compose/runtime/ProvidableAmbient; + public static final fun navigationBarWidth (Landroidx/compose/ui/Modifier;Ldev/chrisbanes/accompanist/insetter/HorizontalSide;)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsHeight (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsHeightPlus-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsPadding (Landroidx/compose/ui/Modifier;ZZZ)Landroidx/compose/ui/Modifier; + public static synthetic fun navigationBarsPadding$default (Landroidx/compose/ui/Modifier;ZZZILjava/lang/Object;)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsWidthPlus-gYsdZ8Q (Landroidx/compose/ui/Modifier;Ldev/chrisbanes/accompanist/insetter/HorizontalSide;F)Landroidx/compose/ui/Modifier; + public static final fun statusBarsHeight (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + public static final fun statusBarsHeight-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; + public static synthetic fun statusBarsHeight-wxomhCo$default (Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier; + public static final fun statusBarsHeightPlus-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; + public static final fun statusBarsPadding (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + public static final fun systemBarsPadding (Landroidx/compose/ui/Modifier;Z)Landroidx/compose/ui/Modifier; + public static synthetic fun systemBarsPadding$default (Landroidx/compose/ui/Modifier;ZILjava/lang/Object;)Landroidx/compose/ui/Modifier; + public static final fun toPaddingValues (Ldev/chrisbanes/accompanist/insetter/Insets;ZZZZLandroidx/compose/runtime/Composer;II)Landroidx/compose/foundation/layout/PaddingValues; +} + +public final class dev/chrisbanes/accompanist/insetter/VerticalSide : java/lang/Enum { + public static final field Bottom Ldev/chrisbanes/accompanist/insetter/VerticalSide; + public static final field Top Ldev/chrisbanes/accompanist/insetter/VerticalSide; + public static final fun valueOf (Ljava/lang/String;)Ldev/chrisbanes/accompanist/insetter/VerticalSide; + public static final fun values ()[Ldev/chrisbanes/accompanist/insetter/VerticalSide; +} + diff --git a/insetter/build.gradle b/insetter/build.gradle index 86ee7f8a7..d24b963fa 100644 --- a/insetter/build.gradle +++ b/insetter/build.gradle @@ -70,7 +70,7 @@ afterEvaluate { } dependencies { - implementation Libs.AndroidX.coreKtx + implementation Libs.AndroidX.coreAlpha implementation Libs.AndroidX.Compose.runtime implementation Libs.AndroidX.Compose.foundation diff --git a/insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt b/insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt index bd30b0de1..89748c7b2 100644 --- a/insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt +++ b/insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt @@ -27,6 +27,4 @@ import org.junit.runners.JUnit4 class InsetterTest { @get:Rule val composeTestRule = createComposeRule() - - } diff --git a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt b/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt new file mode 100644 index 000000000..d011e7c8f --- /dev/null +++ b/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt @@ -0,0 +1,608 @@ +/* + * Copyright 2020 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. + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused") + +package dev.chrisbanes.accompanist.insetter + +import android.view.View +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Providers +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.onCommit +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticAmbientOf +import androidx.compose.ui.LayoutModifier +import androidx.compose.ui.Measurable +import androidx.compose.ui.MeasureScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.layout.IntrinsicMeasurable +import androidx.compose.ui.layout.IntrinsicMeasureScope +import androidx.compose.ui.platform.DensityAmbient +import androidx.compose.ui.platform.LayoutDirectionAmbient +import androidx.compose.ui.platform.ViewAmbient +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.offset +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsCompat.Type + +/** + * Main holder of our inset values. + */ +@Stable +class DisplayInsets { + /** + * Inset values which match [WindowInsetsCompat.Type.systemBars] + */ + val systemBars = Insets() + + /** + * Inset values which match [WindowInsetsCompat.Type.systemGestures] + */ + val systemGestures = Insets() + + /** + * Inset values which match [WindowInsetsCompat.Type.navigationBars] + */ + val navigationBars = Insets() + + /** + * Inset values which match [WindowInsetsCompat.Type.statusBars] + */ + val statusBars = Insets() + + /** + * Inset values which match [WindowInsetsCompat.Type.ime] + */ + val ime = Insets() +} + +@Stable +class Insets { + /** + * The left dimension of these insets in pixels. + */ + var left by mutableStateOf(0) + internal set + + /** + * The top dimension of these insets in pixels. + */ + var top by mutableStateOf(0) + internal set + + /** + * The right dimension of these insets in pixels. + */ + var right by mutableStateOf(0) + internal set + + /** + * The bottom dimension of these insets in pixels. + */ + var bottom by mutableStateOf(0) + internal set + + /** + * Whether the insets are currently visible. + */ + var isVisible by mutableStateOf(true) + internal set +} + +val AmbientInsets = staticAmbientOf { + error("AmbientInsets value not available. Are you using ProvideDisplayInsets?") +} + +/** + * Applies any [WindowInsetsCompat] values to [AmbientInsets], which are then available + * within [content]. + * + * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are dispatched to + * the host view. Defaults to `true`. + */ +@Composable +fun ProvideDisplayInsets( + consumeWindowInsets: Boolean = true, + content: @Composable () -> Unit +) { + val view = ViewAmbient.current + + val displayInsets = remember { DisplayInsets() } + + onCommit(view) { + ViewCompat.setOnApplyWindowInsetsListener(view) { _, windowInsets -> + displayInsets.systemBars.updateFrom(windowInsets, Type.systemBars()) + displayInsets.systemGestures.updateFrom(windowInsets, Type.systemGestures()) + displayInsets.statusBars.updateFrom(windowInsets, Type.statusBars()) + displayInsets.navigationBars.updateFrom(windowInsets, Type.navigationBars()) + displayInsets.ime.updateFrom(windowInsets, Type.ime()) + + if (consumeWindowInsets) WindowInsetsCompat.CONSUMED else windowInsets + } + + // Add an OnAttachStateChangeListener to request an inset pass each time we're attached + // to the window + val attachListener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) = v.requestApplyInsets() + override fun onViewDetachedFromWindow(v: View) = Unit + } + view.addOnAttachStateChangeListener(attachListener) + + if (view.isAttachedToWindow) { + // If the view is already attached, we can request an inset pass now + view.requestApplyInsets() + } + + onDispose { + view.removeOnAttachStateChangeListener(attachListener) + } + } + + Providers(AmbientInsets provides displayInsets) { + content() + } +} + +/** + * Updates our mutable state backed [Insets] from an Android system insets. + */ +private fun Insets.updateFrom(windowInsets: WindowInsetsCompat, type: Int) { + val insets = windowInsets.getInsets(type) + left = insets.left + top = insets.top + right = insets.right + bottom = insets.bottom + + isVisible = windowInsets.isVisible(type) +} + +/** + * Selectively apply additional space which matches the width/height of any system bars present + * on the respective edges of the screen. + * + * @param enabled Whether to apply padding using the system bars dimensions on the respective edges. + * Defaults to `true`. + */ +fun Modifier.systemBarsPadding(enabled: Boolean = true) = composed { + insetsPadding( + insets = AmbientInsets.current.systemBars, + left = enabled, + top = enabled, + right = enabled, + bottom = enabled + ) +} + +/** + * Apply additional space which matches the height of the status bars height along the top edge + * of the content. + */ +fun Modifier.statusBarsPadding() = composed { + insetsPadding(insets = AmbientInsets.current.statusBars, top = true) +} + +/** + * Apply additional space which matches the height of the navigation bars height + * along the [bottom] edge of the content, and additional space which matches the width of + * the navigation bars on the respective [left] and [right] edges. + * + * @param bottom Whether to apply padding to the bottom edge, which matches the navigation bars + * height (if present) at the bottom edge of the screen. Defaults to `true`. + * @param left Whether to apply padding to the left edge, which matches the navigation bars width + * (if present) on the left edge of the screen. Defaults to `true`. + * @param right Whether to apply padding to the right edge, which matches the navigation bars width + * (if present) on the right edge of the screen. Defaults to `true`. + */ +fun Modifier.navigationBarsPadding( + bottom: Boolean = true, + left: Boolean = true, + right: Boolean = true +) = composed { + insetsPadding( + insets = AmbientInsets.current.navigationBars, + left = left, + right = right, + bottom = bottom + ) +} + +/** + * Declare the height of the content to match the height of the status bars exactly. + * + * This is very handy when used with `Spacer` to push content below the status bars: + * ``` + * Column { + * Spacer(Modifier.statusBarHeight()) + * + * // Content to be drawn below status bars (y-axis) + * } + * ``` + * + * It's also useful when used to draw a scrim which matches the status bars: + * ``` + * Spacer( + * Modifier.statusBarHeight() + * .fillMaxWidth() + * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) + * ) + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param additional Any additional height to add to the status bars size. + */ +fun Modifier.statusBarsHeight(additional: Dp = 0.dp) = composed { + InsetsSizeModifier( + insets = AmbientInsets.current.statusBars, + heightSide = VerticalSide.Top, + additionalHeight = additional + ) +} + +/** + * Declare the height of the content to match the height of the status bars exactly. + * + * This is very handy when used with `Spacer` to push content below the status bars: + * ``` + * Column { + * Spacer(Modifier.statusBarHeight()) + * + * // Content to be drawn below status bars (y-axis) + * } + * ``` + * + * It's also useful when used to draw a scrim which matches the status bars: + * ``` + * Spacer( + * Modifier.statusBarHeight() + * .fillMaxWidth() + * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) + * ) + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + */ +inline fun Modifier.statusBarsHeight() = statusBarsHeightPlus(0.dp) + +/** + * Declare the height of the content to match the height of the status bars, plus some + * additional height passed in via [additional]. + * + * As an example, this could be used with `Spacer` to push content below the status bar + * and app bars: + * + * ``` + * Column { + * Spacer(Modifier.statusBarHeightPlus(56.dp)) + * + * // Content to be drawn below status bars and app bar (y-axis) + * } + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param additional Any additional height to add to the status bars size. + */ +fun Modifier.statusBarsHeightPlus(additional: Dp) = composed { + InsetsSizeModifier( + insets = AmbientInsets.current.statusBars, + heightSide = VerticalSide.Top, + additionalHeight = additional + ) +} + +/** + * Declare the preferred height of the content to match the height of the navigation bars when + * present at the bottom of the screen. + * + * This is very handy when used with `Spacer` to push content below the navigation bars: + * ``` + * Column { + * // Content to be drawn above status bars (y-axis) + * Spacer(Modifier.navigationBarHeight()) + * } + * ``` + * + * It's also useful when used to draw a scrim which matches the navigation bars: + * ``` + * Spacer( + * Modifier.navigationBarHeight() + * .fillMaxWidth() + * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) + * ) + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + */ +inline fun Modifier.navigationBarsHeight() = navigationBarsHeightPlus(0.dp) + +/** + * Declare the height of the content to match the height of the navigation bars, plus some + * additional height passed in via [additional] + * + * As an example, this could be used with `Spacer` to push content above the navigation bar + * and bottom app bars: + * + * ``` + * Column { + * // Content to be drawn above navigation bars and bottom app bar (y-axis) + * + * Spacer(Modifier.statusBarHeightPlus(48.dp)) + * } + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param additional Any additional height to add to the status bars size. + */ +fun Modifier.navigationBarsHeightPlus(additional: Dp) = composed { + InsetsSizeModifier( + insets = AmbientInsets.current.navigationBars, + heightSide = VerticalSide.Bottom, + additionalHeight = additional + ) +} + +enum class HorizontalSide { Left, Right } +enum class VerticalSide { Top, Bottom } + +/** + * Declare the preferred width of the content to match the width of the navigation bars, + * on the given [side]. + * + * This is very handy when used with `Spacer` to push content inside from any vertical + * navigation bars (typically when the device is in landscape): + * ``` + * Row { + * Spacer(Modifier.navigationBarWidth(HorizontalSide.Left)) + * + * // Content to be inside the navigation bars (x-axis) + * + * Spacer(Modifier.navigationBarWidth(HorizontalSide.Right)) + * } + * ``` + * + * It's also useful when used to draw a scrim which matches the navigation bars: + * ``` + * Spacer( + * Modifier.navigationBarWidth(HorizontalSide.Left) + * .fillMaxHeight() + * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) + * ) + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param side The navigation bar side to use as the source for the width. + */ +inline fun Modifier.navigationBarWidth(side: HorizontalSide) = navigationBarsWidthPlus(side, 0.dp) + +/** + * Declare the preferred width of the content to match the width of the navigation bars, + * on the given [side], plus additional width passed in via [additional]. + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param side The navigation bar side to use as the source for the width. + * @param additional Any additional width to add to the status bars size. + */ +fun Modifier.navigationBarsWidthPlus( + side: HorizontalSide, + additional: Dp +) = composed { + InsetsSizeModifier( + insets = AmbientInsets.current.navigationBars, + widthSide = side, + additionalWidth = additional + ) +} + +/** + * Returns the current insets converted into a [PaddingValues]. + * + * @param start Whether to apply the inset on the start dimension. + * @param top Whether to apply the inset on the top dimension. + * @param end Whether to apply the inset on the end dimension. + * @param bottom Whether to apply the inset on the bottom dimension. + */ +@Composable +fun Insets.toPaddingValues( + start: Boolean = true, + top: Boolean = true, + end: Boolean = true, + bottom: Boolean = true +): PaddingValues = with(DensityAmbient.current) { + val layoutDirection = LayoutDirectionAmbient.current + PaddingValues( + start = when { + start && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.left.toDp() + start && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.right.toDp() + else -> 0.dp + }, + top = when { + top -> this@toPaddingValues.top.toDp() + else -> 0.dp + }, + end = when { + end && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.right.toDp() + end && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.left.toDp() + else -> 0.dp + }, + bottom = when { + bottom -> this@toPaddingValues.bottom.toDp() + else -> 0.dp + } + ) +} + +/** + * Allows conditional setting of [insets] on each dimension. + */ +private inline fun Modifier.insetsPadding( + insets: Insets, + left: Boolean = false, + top: Boolean = false, + right: Boolean = false, + bottom: Boolean = false +) = this then InsetsPaddingModifier(insets, left, top, right, bottom) + +private data class InsetsPaddingModifier( + private val insets: Insets, + private val applyLeft: Boolean = false, + private val applyTop: Boolean = false, + private val applyRight: Boolean = false, + private val applyBottom: Boolean = false +) : LayoutModifier { + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureScope.MeasureResult { + val left = if (applyLeft) insets.left else 0 + val top = if (applyTop) insets.top else 0 + val right = if (applyRight) insets.right else 0 + val bottom = if (applyBottom) insets.bottom else 0 + val horizontal = left + right + val vertical = top + bottom + + val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) + + val width = (placeable.width + horizontal) + .coerceIn(constraints.minWidth, constraints.maxWidth) + val height = (placeable.height + vertical) + .coerceIn(constraints.minHeight, constraints.maxHeight) + return layout(width, height) { + placeable.place(left, top) + } + } +} + +private data class InsetsSizeModifier( + private val insets: Insets, + private val widthSide: HorizontalSide? = null, + private val additionalWidth: Dp = 0.dp, + private val heightSide: VerticalSide? = null, + private val additionalHeight: Dp = 0.dp +) : LayoutModifier { + private val Density.targetConstraints: Constraints + get() { + val additionalWidthPx = additionalWidth.toIntPx() + val additionalHeightPx = additionalHeight.toIntPx() + return Constraints( + minWidth = additionalWidthPx + when (widthSide) { + HorizontalSide.Left -> insets.left + HorizontalSide.Right -> insets.right + null -> 0 + }, + minHeight = additionalHeightPx + when (heightSide) { + VerticalSide.Top -> insets.top + VerticalSide.Bottom -> insets.bottom + null -> 0 + }, + maxWidth = when (widthSide) { + HorizontalSide.Left -> insets.left + additionalWidthPx + HorizontalSide.Right -> insets.right + additionalWidthPx + null -> Constraints.Infinity + }, + maxHeight = when (heightSide) { + VerticalSide.Top -> insets.top + additionalHeightPx + VerticalSide.Bottom -> insets.bottom + additionalHeightPx + null -> Constraints.Infinity + } + ) + } + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureScope.MeasureResult { + val wrappedConstraints = targetConstraints.let { targetConstraints -> + val resolvedMinWidth = if (widthSide != null) { + targetConstraints.minWidth + } else { + constraints.minWidth.coerceAtMost(targetConstraints.maxWidth) + } + val resolvedMaxWidth = if (widthSide != null) { + targetConstraints.maxWidth + } else { + constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth) + } + val resolvedMinHeight = if (heightSide != null) { + targetConstraints.minHeight + } else { + constraints.minHeight.coerceAtMost(targetConstraints.maxHeight) + } + val resolvedMaxHeight = if (heightSide != null) { + targetConstraints.maxHeight + } else { + constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight) + } + Constraints( + resolvedMinWidth, + resolvedMaxWidth, + resolvedMinHeight, + resolvedMaxHeight + ) + } + val placeable = measurable.measure(wrappedConstraints) + return layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + + override fun IntrinsicMeasureScope.minIntrinsicWidth( + measurable: IntrinsicMeasurable, + height: Int + ) = measurable.minIntrinsicWidth(height).let { + val constraints = targetConstraints + it.coerceIn(constraints.minWidth, constraints.maxWidth) + } + + override fun IntrinsicMeasureScope.maxIntrinsicWidth( + measurable: IntrinsicMeasurable, + height: Int + ) = measurable.maxIntrinsicWidth(height).let { + val constraints = targetConstraints + it.coerceIn(constraints.minWidth, constraints.maxWidth) + } + + override fun IntrinsicMeasureScope.minIntrinsicHeight( + measurable: IntrinsicMeasurable, + width: Int + ) = measurable.minIntrinsicHeight(width).let { + val constraints = targetConstraints + it.coerceIn(constraints.minHeight, constraints.maxHeight) + } + + override fun IntrinsicMeasureScope.maxIntrinsicHeight( + measurable: IntrinsicMeasurable, + width: Int + ) = measurable.maxIntrinsicHeight(width).let { + val constraints = targetConstraints + it.coerceIn(constraints.minHeight, constraints.maxHeight) + } +} diff --git a/sample/build.gradle b/sample/build.gradle index c413fb0e7..20b5a3048 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -60,10 +60,6 @@ dependencies { implementation Libs.AndroidX.Compose.material implementation Libs.AndroidX.Compose.foundation implementation Libs.AndroidX.Compose.layout - implementation Libs.AndroidX.Compose.material - - implementation Libs.AndroidX.coreKtx - implementation Libs.AndroidX.appcompat implementation Libs.Kotlin.stdlib } From 7bc45bdf3a2c5d75d1b488eeb377e08905107482 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Thu, 29 Oct 2020 16:34:17 +0000 Subject: [PATCH 03/22] Split out Padding and Size modifiers --- insetter/api/insetter.api | 8 +- .../accompanist/insetter/Insetter.kt | 443 +----------------- .../accompanist/insetter/Padding.kt | 121 +++++ .../chrisbanes/accompanist/insetter/Size.kt | 372 +++++++++++++++ 4 files changed, 500 insertions(+), 444 deletions(-) create mode 100644 insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Padding.kt create mode 100644 insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Size.kt diff --git a/insetter/api/insetter.api b/insetter/api/insetter.api index dbb921d01..a5e2a5130 100644 --- a/insetter/api/insetter.api +++ b/insetter/api/insetter.api @@ -23,22 +23,22 @@ public final class dev/chrisbanes/accompanist/insetter/Insets { public final fun isVisible ()Z } -public final class dev/chrisbanes/accompanist/insetter/InsetterKt { +public final class dev/chrisbanes/accompanist/insetter/Insetter { public static final fun ProvideDisplayInsets (ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V public static final fun getAmbientInsets ()Landroidx/compose/runtime/ProvidableAmbient; public static final fun navigationBarWidth (Landroidx/compose/ui/Modifier;Ldev/chrisbanes/accompanist/insetter/HorizontalSide;)Landroidx/compose/ui/Modifier; public static final fun navigationBarsHeight (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; public static final fun navigationBarsHeightPlus-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; public static final fun navigationBarsPadding (Landroidx/compose/ui/Modifier;ZZZ)Landroidx/compose/ui/Modifier; - public static synthetic fun navigationBarsPadding$default (Landroidx/compose/ui/Modifier;ZZZILjava/lang/Object;)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsPadding$default (Landroidx/compose/ui/Modifier;ZZZILjava/lang/Object;)Landroidx/compose/ui/Modifier; public static final fun navigationBarsWidthPlus-gYsdZ8Q (Landroidx/compose/ui/Modifier;Ldev/chrisbanes/accompanist/insetter/HorizontalSide;F)Landroidx/compose/ui/Modifier; public static final fun statusBarsHeight (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; public static final fun statusBarsHeight-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; - public static synthetic fun statusBarsHeight-wxomhCo$default (Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier; + public static final fun statusBarsHeight-wxomhCo$default (Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier; public static final fun statusBarsHeightPlus-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; public static final fun statusBarsPadding (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; public static final fun systemBarsPadding (Landroidx/compose/ui/Modifier;Z)Landroidx/compose/ui/Modifier; - public static synthetic fun systemBarsPadding$default (Landroidx/compose/ui/Modifier;ZILjava/lang/Object;)Landroidx/compose/ui/Modifier; + public static final fun systemBarsPadding$default (Landroidx/compose/ui/Modifier;ZILjava/lang/Object;)Landroidx/compose/ui/Modifier; public static final fun toPaddingValues (Ldev/chrisbanes/accompanist/insetter/Insets;ZZZZLandroidx/compose/runtime/Composer;II)Landroidx/compose/foundation/layout/PaddingValues; } diff --git a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt b/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt index d011e7c8f..6053bd5be 100644 --- a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt +++ b/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt @@ -16,11 +16,12 @@ @file:Suppress("NOTHING_TO_INLINE", "unused") +@file:JvmName("Insetter") +@file:JvmMultifileClass + package dev.chrisbanes.accompanist.insetter import android.view.View -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.Providers import androidx.compose.runtime.Stable @@ -30,22 +31,7 @@ import androidx.compose.runtime.onCommit import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.staticAmbientOf -import androidx.compose.ui.LayoutModifier -import androidx.compose.ui.Measurable -import androidx.compose.ui.MeasureScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.layout.IntrinsicMeasurable -import androidx.compose.ui.layout.IntrinsicMeasureScope -import androidx.compose.ui.platform.DensityAmbient -import androidx.compose.ui.platform.LayoutDirectionAmbient import androidx.compose.ui.platform.ViewAmbient -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.offset import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat.Type @@ -181,428 +167,5 @@ private fun Insets.updateFrom(windowInsets: WindowInsetsCompat, type: Int) { isVisible = windowInsets.isVisible(type) } -/** - * Selectively apply additional space which matches the width/height of any system bars present - * on the respective edges of the screen. - * - * @param enabled Whether to apply padding using the system bars dimensions on the respective edges. - * Defaults to `true`. - */ -fun Modifier.systemBarsPadding(enabled: Boolean = true) = composed { - insetsPadding( - insets = AmbientInsets.current.systemBars, - left = enabled, - top = enabled, - right = enabled, - bottom = enabled - ) -} - -/** - * Apply additional space which matches the height of the status bars height along the top edge - * of the content. - */ -fun Modifier.statusBarsPadding() = composed { - insetsPadding(insets = AmbientInsets.current.statusBars, top = true) -} - -/** - * Apply additional space which matches the height of the navigation bars height - * along the [bottom] edge of the content, and additional space which matches the width of - * the navigation bars on the respective [left] and [right] edges. - * - * @param bottom Whether to apply padding to the bottom edge, which matches the navigation bars - * height (if present) at the bottom edge of the screen. Defaults to `true`. - * @param left Whether to apply padding to the left edge, which matches the navigation bars width - * (if present) on the left edge of the screen. Defaults to `true`. - * @param right Whether to apply padding to the right edge, which matches the navigation bars width - * (if present) on the right edge of the screen. Defaults to `true`. - */ -fun Modifier.navigationBarsPadding( - bottom: Boolean = true, - left: Boolean = true, - right: Boolean = true -) = composed { - insetsPadding( - insets = AmbientInsets.current.navigationBars, - left = left, - right = right, - bottom = bottom - ) -} - -/** - * Declare the height of the content to match the height of the status bars exactly. - * - * This is very handy when used with `Spacer` to push content below the status bars: - * ``` - * Column { - * Spacer(Modifier.statusBarHeight()) - * - * // Content to be drawn below status bars (y-axis) - * } - * ``` - * - * It's also useful when used to draw a scrim which matches the status bars: - * ``` - * Spacer( - * Modifier.statusBarHeight() - * .fillMaxWidth() - * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) - * ) - * ``` - * - * Internally this matches the behavior of the [Modifier.height] modifier. - * - * @param additional Any additional height to add to the status bars size. - */ -fun Modifier.statusBarsHeight(additional: Dp = 0.dp) = composed { - InsetsSizeModifier( - insets = AmbientInsets.current.statusBars, - heightSide = VerticalSide.Top, - additionalHeight = additional - ) -} - -/** - * Declare the height of the content to match the height of the status bars exactly. - * - * This is very handy when used with `Spacer` to push content below the status bars: - * ``` - * Column { - * Spacer(Modifier.statusBarHeight()) - * - * // Content to be drawn below status bars (y-axis) - * } - * ``` - * - * It's also useful when used to draw a scrim which matches the status bars: - * ``` - * Spacer( - * Modifier.statusBarHeight() - * .fillMaxWidth() - * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) - * ) - * ``` - * - * Internally this matches the behavior of the [Modifier.height] modifier. - */ -inline fun Modifier.statusBarsHeight() = statusBarsHeightPlus(0.dp) - -/** - * Declare the height of the content to match the height of the status bars, plus some - * additional height passed in via [additional]. - * - * As an example, this could be used with `Spacer` to push content below the status bar - * and app bars: - * - * ``` - * Column { - * Spacer(Modifier.statusBarHeightPlus(56.dp)) - * - * // Content to be drawn below status bars and app bar (y-axis) - * } - * ``` - * - * Internally this matches the behavior of the [Modifier.height] modifier. - * - * @param additional Any additional height to add to the status bars size. - */ -fun Modifier.statusBarsHeightPlus(additional: Dp) = composed { - InsetsSizeModifier( - insets = AmbientInsets.current.statusBars, - heightSide = VerticalSide.Top, - additionalHeight = additional - ) -} - -/** - * Declare the preferred height of the content to match the height of the navigation bars when - * present at the bottom of the screen. - * - * This is very handy when used with `Spacer` to push content below the navigation bars: - * ``` - * Column { - * // Content to be drawn above status bars (y-axis) - * Spacer(Modifier.navigationBarHeight()) - * } - * ``` - * - * It's also useful when used to draw a scrim which matches the navigation bars: - * ``` - * Spacer( - * Modifier.navigationBarHeight() - * .fillMaxWidth() - * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) - * ) - * ``` - * - * Internally this matches the behavior of the [Modifier.height] modifier. - */ -inline fun Modifier.navigationBarsHeight() = navigationBarsHeightPlus(0.dp) - -/** - * Declare the height of the content to match the height of the navigation bars, plus some - * additional height passed in via [additional] - * - * As an example, this could be used with `Spacer` to push content above the navigation bar - * and bottom app bars: - * - * ``` - * Column { - * // Content to be drawn above navigation bars and bottom app bar (y-axis) - * - * Spacer(Modifier.statusBarHeightPlus(48.dp)) - * } - * ``` - * - * Internally this matches the behavior of the [Modifier.height] modifier. - * - * @param additional Any additional height to add to the status bars size. - */ -fun Modifier.navigationBarsHeightPlus(additional: Dp) = composed { - InsetsSizeModifier( - insets = AmbientInsets.current.navigationBars, - heightSide = VerticalSide.Bottom, - additionalHeight = additional - ) -} - enum class HorizontalSide { Left, Right } enum class VerticalSide { Top, Bottom } - -/** - * Declare the preferred width of the content to match the width of the navigation bars, - * on the given [side]. - * - * This is very handy when used with `Spacer` to push content inside from any vertical - * navigation bars (typically when the device is in landscape): - * ``` - * Row { - * Spacer(Modifier.navigationBarWidth(HorizontalSide.Left)) - * - * // Content to be inside the navigation bars (x-axis) - * - * Spacer(Modifier.navigationBarWidth(HorizontalSide.Right)) - * } - * ``` - * - * It's also useful when used to draw a scrim which matches the navigation bars: - * ``` - * Spacer( - * Modifier.navigationBarWidth(HorizontalSide.Left) - * .fillMaxHeight() - * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) - * ) - * ``` - * - * Internally this matches the behavior of the [Modifier.height] modifier. - * - * @param side The navigation bar side to use as the source for the width. - */ -inline fun Modifier.navigationBarWidth(side: HorizontalSide) = navigationBarsWidthPlus(side, 0.dp) - -/** - * Declare the preferred width of the content to match the width of the navigation bars, - * on the given [side], plus additional width passed in via [additional]. - * - * Internally this matches the behavior of the [Modifier.height] modifier. - * - * @param side The navigation bar side to use as the source for the width. - * @param additional Any additional width to add to the status bars size. - */ -fun Modifier.navigationBarsWidthPlus( - side: HorizontalSide, - additional: Dp -) = composed { - InsetsSizeModifier( - insets = AmbientInsets.current.navigationBars, - widthSide = side, - additionalWidth = additional - ) -} - -/** - * Returns the current insets converted into a [PaddingValues]. - * - * @param start Whether to apply the inset on the start dimension. - * @param top Whether to apply the inset on the top dimension. - * @param end Whether to apply the inset on the end dimension. - * @param bottom Whether to apply the inset on the bottom dimension. - */ -@Composable -fun Insets.toPaddingValues( - start: Boolean = true, - top: Boolean = true, - end: Boolean = true, - bottom: Boolean = true -): PaddingValues = with(DensityAmbient.current) { - val layoutDirection = LayoutDirectionAmbient.current - PaddingValues( - start = when { - start && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.left.toDp() - start && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.right.toDp() - else -> 0.dp - }, - top = when { - top -> this@toPaddingValues.top.toDp() - else -> 0.dp - }, - end = when { - end && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.right.toDp() - end && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.left.toDp() - else -> 0.dp - }, - bottom = when { - bottom -> this@toPaddingValues.bottom.toDp() - else -> 0.dp - } - ) -} - -/** - * Allows conditional setting of [insets] on each dimension. - */ -private inline fun Modifier.insetsPadding( - insets: Insets, - left: Boolean = false, - top: Boolean = false, - right: Boolean = false, - bottom: Boolean = false -) = this then InsetsPaddingModifier(insets, left, top, right, bottom) - -private data class InsetsPaddingModifier( - private val insets: Insets, - private val applyLeft: Boolean = false, - private val applyTop: Boolean = false, - private val applyRight: Boolean = false, - private val applyBottom: Boolean = false -) : LayoutModifier { - override fun MeasureScope.measure( - measurable: Measurable, - constraints: Constraints - ): MeasureScope.MeasureResult { - val left = if (applyLeft) insets.left else 0 - val top = if (applyTop) insets.top else 0 - val right = if (applyRight) insets.right else 0 - val bottom = if (applyBottom) insets.bottom else 0 - val horizontal = left + right - val vertical = top + bottom - - val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) - - val width = (placeable.width + horizontal) - .coerceIn(constraints.minWidth, constraints.maxWidth) - val height = (placeable.height + vertical) - .coerceIn(constraints.minHeight, constraints.maxHeight) - return layout(width, height) { - placeable.place(left, top) - } - } -} - -private data class InsetsSizeModifier( - private val insets: Insets, - private val widthSide: HorizontalSide? = null, - private val additionalWidth: Dp = 0.dp, - private val heightSide: VerticalSide? = null, - private val additionalHeight: Dp = 0.dp -) : LayoutModifier { - private val Density.targetConstraints: Constraints - get() { - val additionalWidthPx = additionalWidth.toIntPx() - val additionalHeightPx = additionalHeight.toIntPx() - return Constraints( - minWidth = additionalWidthPx + when (widthSide) { - HorizontalSide.Left -> insets.left - HorizontalSide.Right -> insets.right - null -> 0 - }, - minHeight = additionalHeightPx + when (heightSide) { - VerticalSide.Top -> insets.top - VerticalSide.Bottom -> insets.bottom - null -> 0 - }, - maxWidth = when (widthSide) { - HorizontalSide.Left -> insets.left + additionalWidthPx - HorizontalSide.Right -> insets.right + additionalWidthPx - null -> Constraints.Infinity - }, - maxHeight = when (heightSide) { - VerticalSide.Top -> insets.top + additionalHeightPx - VerticalSide.Bottom -> insets.bottom + additionalHeightPx - null -> Constraints.Infinity - } - ) - } - - override fun MeasureScope.measure( - measurable: Measurable, - constraints: Constraints - ): MeasureScope.MeasureResult { - val wrappedConstraints = targetConstraints.let { targetConstraints -> - val resolvedMinWidth = if (widthSide != null) { - targetConstraints.minWidth - } else { - constraints.minWidth.coerceAtMost(targetConstraints.maxWidth) - } - val resolvedMaxWidth = if (widthSide != null) { - targetConstraints.maxWidth - } else { - constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth) - } - val resolvedMinHeight = if (heightSide != null) { - targetConstraints.minHeight - } else { - constraints.minHeight.coerceAtMost(targetConstraints.maxHeight) - } - val resolvedMaxHeight = if (heightSide != null) { - targetConstraints.maxHeight - } else { - constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight) - } - Constraints( - resolvedMinWidth, - resolvedMaxWidth, - resolvedMinHeight, - resolvedMaxHeight - ) - } - val placeable = measurable.measure(wrappedConstraints) - return layout(placeable.width, placeable.height) { - placeable.place(0, 0) - } - } - - override fun IntrinsicMeasureScope.minIntrinsicWidth( - measurable: IntrinsicMeasurable, - height: Int - ) = measurable.minIntrinsicWidth(height).let { - val constraints = targetConstraints - it.coerceIn(constraints.minWidth, constraints.maxWidth) - } - - override fun IntrinsicMeasureScope.maxIntrinsicWidth( - measurable: IntrinsicMeasurable, - height: Int - ) = measurable.maxIntrinsicWidth(height).let { - val constraints = targetConstraints - it.coerceIn(constraints.minWidth, constraints.maxWidth) - } - - override fun IntrinsicMeasureScope.minIntrinsicHeight( - measurable: IntrinsicMeasurable, - width: Int - ) = measurable.minIntrinsicHeight(width).let { - val constraints = targetConstraints - it.coerceIn(constraints.minHeight, constraints.maxHeight) - } - - override fun IntrinsicMeasureScope.maxIntrinsicHeight( - measurable: IntrinsicMeasurable, - width: Int - ) = measurable.maxIntrinsicHeight(width).let { - val constraints = targetConstraints - it.coerceIn(constraints.minHeight, constraints.maxHeight) - } -} diff --git a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Padding.kt b/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Padding.kt new file mode 100644 index 000000000..f3bb3cfc4 --- /dev/null +++ b/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Padding.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2020 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. + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused") + +@file:JvmName("Insetter") +@file:JvmMultifileClass + +package dev.chrisbanes.accompanist.insetter + +import androidx.compose.ui.LayoutModifier +import androidx.compose.ui.Measurable +import androidx.compose.ui.MeasureScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.offset + +/** + * Selectively apply additional space which matches the width/height of any system bars present + * on the respective edges of the screen. + * + * @param enabled Whether to apply padding using the system bars dimensions on the respective edges. + * Defaults to `true`. + */ +fun Modifier.systemBarsPadding(enabled: Boolean = true) = composed { + insetsPadding( + insets = AmbientInsets.current.systemBars, + left = enabled, + top = enabled, + right = enabled, + bottom = enabled + ) +} + +/** + * Apply additional space which matches the height of the status bars height along the top edge + * of the content. + */ +fun Modifier.statusBarsPadding() = composed { + insetsPadding(insets = AmbientInsets.current.statusBars, top = true) +} + +/** + * Apply additional space which matches the height of the navigation bars height + * along the [bottom] edge of the content, and additional space which matches the width of + * the navigation bars on the respective [left] and [right] edges. + * + * @param bottom Whether to apply padding to the bottom edge, which matches the navigation bars + * height (if present) at the bottom edge of the screen. Defaults to `true`. + * @param left Whether to apply padding to the left edge, which matches the navigation bars width + * (if present) on the left edge of the screen. Defaults to `true`. + * @param right Whether to apply padding to the right edge, which matches the navigation bars width + * (if present) on the right edge of the screen. Defaults to `true`. + */ +fun Modifier.navigationBarsPadding( + bottom: Boolean = true, + left: Boolean = true, + right: Boolean = true +) = composed { + insetsPadding( + insets = AmbientInsets.current.navigationBars, + left = left, + right = right, + bottom = bottom + ) +} + +/** + * Allows conditional setting of [insets] on each dimension. + */ +private inline fun Modifier.insetsPadding( + insets: Insets, + left: Boolean = false, + top: Boolean = false, + right: Boolean = false, + bottom: Boolean = false, +) = this then InsetsPaddingModifier(insets, left, top, right, bottom) + +private data class InsetsPaddingModifier( + private val insets: Insets, + private val applyLeft: Boolean, + private val applyTop: Boolean, + private val applyRight: Boolean, + private val applyBottom: Boolean +) : LayoutModifier { + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureScope.MeasureResult { + val left = if (applyLeft) insets.left else 0 + val top = if (applyTop) insets.top else 0 + val right = if (applyRight) insets.right else 0 + val bottom = if (applyBottom) insets.bottom else 0 + val horizontal = left + right + val vertical = top + bottom + + val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) + + val width = (placeable.width + horizontal) + .coerceIn(constraints.minWidth, constraints.maxWidth) + val height = (placeable.height + vertical) + .coerceIn(constraints.minHeight, constraints.maxHeight) + return layout(width, height) { + placeable.place(left, top) + } + } +} diff --git a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Size.kt b/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Size.kt new file mode 100644 index 000000000..c2cf911bf --- /dev/null +++ b/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Size.kt @@ -0,0 +1,372 @@ +/* + * Copyright 2020 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. + */ + +@file:Suppress("NOTHING_TO_INLINE", "unused") + +@file:JvmName("Insetter") +@file:JvmMultifileClass + +package dev.chrisbanes.accompanist.insetter + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.LayoutModifier +import androidx.compose.ui.Measurable +import androidx.compose.ui.MeasureScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.layout.IntrinsicMeasurable +import androidx.compose.ui.layout.IntrinsicMeasureScope +import androidx.compose.ui.platform.DensityAmbient +import androidx.compose.ui.platform.LayoutDirectionAmbient +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +/** + * Declare the height of the content to match the height of the status bars exactly. + * + * This is very handy when used with `Spacer` to push content below the status bars: + * ``` + * Column { + * Spacer(Modifier.statusBarHeight()) + * + * // Content to be drawn below status bars (y-axis) + * } + * ``` + * + * It's also useful when used to draw a scrim which matches the status bars: + * ``` + * Spacer( + * Modifier.statusBarHeight() + * .fillMaxWidth() + * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) + * ) + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param additional Any additional height to add to the status bars size. + */ +fun Modifier.statusBarsHeight(additional: Dp = 0.dp) = composed { + InsetsSizeModifier( + insets = AmbientInsets.current.statusBars, + heightSide = VerticalSide.Top, + additionalHeight = additional + ) +} + +/** + * Declare the height of the content to match the height of the status bars exactly. + * + * This is very handy when used with `Spacer` to push content below the status bars: + * ``` + * Column { + * Spacer(Modifier.statusBarHeight()) + * + * // Content to be drawn below status bars (y-axis) + * } + * ``` + * + * It's also useful when used to draw a scrim which matches the status bars: + * ``` + * Spacer( + * Modifier.statusBarHeight() + * .fillMaxWidth() + * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) + * ) + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + */ +inline fun Modifier.statusBarsHeight() = statusBarsHeightPlus(0.dp) + +/** + * Declare the height of the content to match the height of the status bars, plus some + * additional height passed in via [additional]. + * + * As an example, this could be used with `Spacer` to push content below the status bar + * and app bars: + * + * ``` + * Column { + * Spacer(Modifier.statusBarHeightPlus(56.dp)) + * + * // Content to be drawn below status bars and app bar (y-axis) + * } + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param additional Any additional height to add to the status bars size. + */ +fun Modifier.statusBarsHeightPlus(additional: Dp) = composed { + InsetsSizeModifier( + insets = AmbientInsets.current.statusBars, + heightSide = VerticalSide.Top, + additionalHeight = additional + ) +} + +/** + * Declare the preferred height of the content to match the height of the navigation bars when + * present at the bottom of the screen. + * + * This is very handy when used with `Spacer` to push content below the navigation bars: + * ``` + * Column { + * // Content to be drawn above status bars (y-axis) + * Spacer(Modifier.navigationBarHeight()) + * } + * ``` + * + * It's also useful when used to draw a scrim which matches the navigation bars: + * ``` + * Spacer( + * Modifier.navigationBarHeight() + * .fillMaxWidth() + * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) + * ) + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + */ +inline fun Modifier.navigationBarsHeight() = navigationBarsHeightPlus(0.dp) + +/** + * Declare the height of the content to match the height of the navigation bars, plus some + * additional height passed in via [additional] + * + * As an example, this could be used with `Spacer` to push content above the navigation bar + * and bottom app bars: + * + * ``` + * Column { + * // Content to be drawn above navigation bars and bottom app bar (y-axis) + * + * Spacer(Modifier.statusBarHeightPlus(48.dp)) + * } + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param additional Any additional height to add to the status bars size. + */ +fun Modifier.navigationBarsHeightPlus(additional: Dp) = composed { + InsetsSizeModifier( + insets = AmbientInsets.current.navigationBars, + heightSide = VerticalSide.Bottom, + additionalHeight = additional + ) +} + +/** + * Declare the preferred width of the content to match the width of the navigation bars, + * on the given [side]. + * + * This is very handy when used with `Spacer` to push content inside from any vertical + * navigation bars (typically when the device is in landscape): + * ``` + * Row { + * Spacer(Modifier.navigationBarWidth(HorizontalSide.Left)) + * + * // Content to be inside the navigation bars (x-axis) + * + * Spacer(Modifier.navigationBarWidth(HorizontalSide.Right)) + * } + * ``` + * + * It's also useful when used to draw a scrim which matches the navigation bars: + * ``` + * Spacer( + * Modifier.navigationBarWidth(HorizontalSide.Left) + * .fillMaxHeight() + * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f) + * ) + * ``` + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param side The navigation bar side to use as the source for the width. + */ +inline fun Modifier.navigationBarWidth(side: HorizontalSide) = navigationBarsWidthPlus(side, 0.dp) + +/** + * Declare the preferred width of the content to match the width of the navigation bars, + * on the given [side], plus additional width passed in via [additional]. + * + * Internally this matches the behavior of the [Modifier.height] modifier. + * + * @param side The navigation bar side to use as the source for the width. + * @param additional Any additional width to add to the status bars size. + */ +fun Modifier.navigationBarsWidthPlus( + side: HorizontalSide, + additional: Dp +) = composed { + InsetsSizeModifier( + insets = AmbientInsets.current.navigationBars, + widthSide = side, + additionalWidth = additional + ) +} + +/** + * Returns the current insets converted into a [PaddingValues]. + * + * @param start Whether to apply the inset on the start dimension. + * @param top Whether to apply the inset on the top dimension. + * @param end Whether to apply the inset on the end dimension. + * @param bottom Whether to apply the inset on the bottom dimension. + */ +@Composable +fun Insets.toPaddingValues( + start: Boolean = true, + top: Boolean = true, + end: Boolean = true, + bottom: Boolean = true +): PaddingValues = with(DensityAmbient.current) { + val layoutDirection = LayoutDirectionAmbient.current + PaddingValues( + start = when { + start && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.left.toDp() + start && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.right.toDp() + else -> 0.dp + }, + top = when { + top -> this@toPaddingValues.top.toDp() + else -> 0.dp + }, + end = when { + end && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.right.toDp() + end && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.left.toDp() + else -> 0.dp + }, + bottom = when { + bottom -> this@toPaddingValues.bottom.toDp() + else -> 0.dp + } + ) +} + +private data class InsetsSizeModifier( + private val insets: Insets, + private val widthSide: HorizontalSide? = null, + private val additionalWidth: Dp = 0.dp, + private val heightSide: VerticalSide? = null, + private val additionalHeight: Dp = 0.dp +) : LayoutModifier { + private val Density.targetConstraints: Constraints + get() { + val additionalWidthPx = additionalWidth.toIntPx() + val additionalHeightPx = additionalHeight.toIntPx() + return Constraints( + minWidth = additionalWidthPx + when (widthSide) { + HorizontalSide.Left -> insets.left + HorizontalSide.Right -> insets.right + null -> 0 + }, + minHeight = additionalHeightPx + when (heightSide) { + VerticalSide.Top -> insets.top + VerticalSide.Bottom -> insets.bottom + null -> 0 + }, + maxWidth = when (widthSide) { + HorizontalSide.Left -> insets.left + additionalWidthPx + HorizontalSide.Right -> insets.right + additionalWidthPx + null -> Constraints.Infinity + }, + maxHeight = when (heightSide) { + VerticalSide.Top -> insets.top + additionalHeightPx + VerticalSide.Bottom -> insets.bottom + additionalHeightPx + null -> Constraints.Infinity + } + ) + } + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureScope.MeasureResult { + val wrappedConstraints = targetConstraints.let { targetConstraints -> + val resolvedMinWidth = if (widthSide != null) { + targetConstraints.minWidth + } else { + constraints.minWidth.coerceAtMost(targetConstraints.maxWidth) + } + val resolvedMaxWidth = if (widthSide != null) { + targetConstraints.maxWidth + } else { + constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth) + } + val resolvedMinHeight = if (heightSide != null) { + targetConstraints.minHeight + } else { + constraints.minHeight.coerceAtMost(targetConstraints.maxHeight) + } + val resolvedMaxHeight = if (heightSide != null) { + targetConstraints.maxHeight + } else { + constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight) + } + Constraints( + resolvedMinWidth, + resolvedMaxWidth, + resolvedMinHeight, + resolvedMaxHeight + ) + } + val placeable = measurable.measure(wrappedConstraints) + return layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + + override fun IntrinsicMeasureScope.minIntrinsicWidth( + measurable: IntrinsicMeasurable, + height: Int + ) = measurable.minIntrinsicWidth(height).let { + val constraints = targetConstraints + it.coerceIn(constraints.minWidth, constraints.maxWidth) + } + + override fun IntrinsicMeasureScope.maxIntrinsicWidth( + measurable: IntrinsicMeasurable, + height: Int + ) = measurable.maxIntrinsicWidth(height).let { + val constraints = targetConstraints + it.coerceIn(constraints.minWidth, constraints.maxWidth) + } + + override fun IntrinsicMeasureScope.minIntrinsicHeight( + measurable: IntrinsicMeasurable, + width: Int + ) = measurable.minIntrinsicHeight(width).let { + val constraints = targetConstraints + it.coerceIn(constraints.minHeight, constraints.maxHeight) + } + + override fun IntrinsicMeasureScope.maxIntrinsicHeight( + measurable: IntrinsicMeasurable, + width: Int + ) = measurable.maxIntrinsicHeight(width).let { + val constraints = targetConstraints + it.coerceIn(constraints.minHeight, constraints.maxHeight) + } +} From 9f9688a0bdbb6c00c406a7ee7fa5d6e048dc9322 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Thu, 29 Oct 2020 17:25:23 +0000 Subject: [PATCH 04/22] Add some rough docs --- README.md | 14 +++++++++++--- generate_docs.sh | 11 ++++++----- insetter/README.md | 46 +++++++++++++++++++++++++++++++++++++++++++++- mkdocs.yml | 1 + 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index fa5e0f382..34bd2db12 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,17 @@ Accompanist is a group of libraries that contains some utilities which I've found myself copying around projects which use [Jetpack Compose][compose]. Currently, it contains: - - 🖼️ [Coil image loading composables](./coil/README.md) - - 🖼️ [Picasso image loading composables](./picasso/README.md) - - 🖼️ [Glide image loading composables](./glide/README.md) +### Image loading +A number of libraries which aim to integrate some popular image loading libraries into Compose: + + - 🖼️ [Coil image loading composables](./coil/) + - 🖼️ [Picasso image loading composables](./picasso/) + - 🖼️ [Glide image loading composables](./glide/) + +### 📐 [Insetter](./insetter/) +TODO + +--- [Jetpack Compose][compose] is a fast-moving project and I'll be updating these libraries to match the latest tagged release as quickly as possible. Each [release listing](https://github.com/chrisbanes/accompanist/releases) will outline what version of Compose libraries it depends on. diff --git a/generate_docs.sh b/generate_docs.sh index 7fe071774..f4d984826 100755 --- a/generate_docs.sh +++ b/generate_docs.sh @@ -25,11 +25,12 @@ cp CONTRIBUTING.md $DOCS_ROOT/contributing.md cp images/social.png $DOCS_ROOT/header.png sed -i.bak 's/CONTRIBUTING.md/contributing/' $DOCS_ROOT/index.md -sed -i.bak 's/coil\/README.md/glide/' $DOCS_ROOT/index.md -sed -i.bak 's/glide\/README.md/coil/' $DOCS_ROOT/index.md -sed -i.bak 's/picasso\/README.md/picasso/' $DOCS_ROOT/index.md +sed -i.bak 's/README.md//' $DOCS_ROOT/index.md sed -i.bak 's/images\/social.png/header.png/' $DOCS_ROOT/index.md +# Convert docs/xxx.md links to just xxx/ +sed -i.bak 's/docs\/\([a-zA-Z-]*\).md/\1/' $DOCS_ROOT/index.md + cp coil/README.md $DOCS_ROOT/coil.md mkdir -p $DOCS_ROOT/coil cp coil/images/crossfade.gif $DOCS_ROOT/coil/crossfade.gif @@ -45,8 +46,8 @@ mkdir -p $DOCS_ROOT/glide cp glide/images/crossfade.gif $DOCS_ROOT/glide/crossfade.gif sed -i.bak 's/images\/crossfade.gif/crossfade.gif/' $DOCS_ROOT/glide.md -# Convert docs/xxx.md links to just xxx/ -sed -i.bak 's/docs\/\([a-zA-Z-]*\).md/\1/' $DOCS_ROOT/index.md +cp insetter/README.md $DOCS_ROOT/insetter.md +mkdir -p $DOCS_ROOT/insetter ######################### # Tidy up Dokka output diff --git a/insetter/README.md b/insetter/README.md index 08effd352..86fc2ee26 100644 --- a/insetter/README.md +++ b/insetter/README.md @@ -1,9 +1,45 @@ -# Jetpack Compose + Insetter +# Insetter for Jetpack Compose [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.chrisbanes.accompanist/accompanist-insetter/badge.svg)](https://search.maven.org/search?q=g:dev.chrisbanes.accompanist) +Insetter for Jetpack Compose takes a lot of the ideas which drove [Insetter][insetter-view] for views, and applies them for use in composables. + +## Usage +To setup Insetter in your composables, you need to call the `ProvideDisplayInsets` function and +wrap your content. This would typically be done near the top level of your composable hierarchy: + +``` kotlin +setContent { + MaterialTheme { + ProvideDisplayInsets { + // your content + } + } +} +``` + +> Note: Whether `ProvideDisplayInsets` is called outside or within `MaterialTheme` doesn't particularly matter. + +`ProvideDisplayInsets` allows Insetter to set an [`OnApplyWindowInsetsListener`][insetslistener] on your content's host view. That listener is used to update the value of an ambient bundled in this library: `AmbientInsetter`. + +`AmbientInsetter` holds an instance of `DisplayInsets` which contains the value of various [WindowInsets][insets] [types][insettypes]. You can use the values manually like so: + +``` kotlin +@Composable +fun ImeAvoidingBox() { + val insets = AmbientInsetter.current + + Box(Modifier.padding(bottom = insets.ime.bottom)) +} +``` + +...but we also provide some easy-to-use [Modifier][modifier]s. + +### Modifiers + TODO + ## Download ```groovy @@ -17,3 +53,11 @@ dependencies { ``` Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit. + +[compose]: https://developer.android.com/jetpack/compose +[snap]: https://oss.sonatype.org/content/repositories/snapshots/dev/chrisbanes/accompanist/accompanist-glide/ +[insetter-view]: https://github.com/chrisbanes/insetter +[insets]: https://developer.android.com/reference/kotlin/androidx/core/view/WindowInsetsCompat +[insettypes]: https://developer.android.com/reference/kotlin/androidx/core/view/WindowInsetsCompat.Type +[insetslistener]: https://developer.android.com/reference/kotlin/androidx/core/view/OnApplyWindowInsetsListener +[modifier]: https://developer.android.com/reference/kotlin/androidx/ui/core/Modifier diff --git a/mkdocs.yml b/mkdocs.yml index 3fe236944..26d3d64c5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,6 +22,7 @@ nav: - 'Glide': api/glide/index.md - 'Picasso': api/picasso/index.md - 'Image Loading Core': api/imageloading-core/index.md + - 'Insetter': api/insetter/index.md - 'Snapshots': using-snapshot-version.md - 'Contributing': contributing.md - 'Maintainers': From fc58be6f1a6b0941dbe6363b1ce0f5d143687662 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Thu, 29 Oct 2020 17:45:01 +0000 Subject: [PATCH 05/22] Rename library to Insets --- README.md | 2 +- generate_docs.sh | 4 +- {insetter => insets}/README.md | 20 +++--- .../api/insetter.api => insets/api/insets.api | 70 +++++++++---------- {insetter => insets}/build.gradle | 0 insets/gradle.properties | 3 + .../src/androidTest/AndroidManifest.xml | 2 +- .../accompanist/insets/InsetsTest.kt | 4 +- .../src/main/AndroidManifest.xml | 2 +- .../chrisbanes/accompanist/insets/Insets.kt | 34 ++++----- .../chrisbanes/accompanist/insets}/Padding.kt | 10 +-- .../chrisbanes/accompanist/insets}/Size.kt | 12 ++-- insetter/gradle.properties | 3 - settings.gradle | 2 +- 14 files changed, 84 insertions(+), 84 deletions(-) rename {insetter => insets}/README.md (57%) rename insetter/api/insetter.api => insets/api/insets.api (65%) rename {insetter => insets}/build.gradle (100%) create mode 100644 insets/gradle.properties rename {insetter => insets}/src/androidTest/AndroidManifest.xml (93%) rename insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt => insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTest.kt (93%) rename {insetter => insets}/src/main/AndroidManifest.xml (91%) rename insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt => insets/src/main/java/dev/chrisbanes/accompanist/insets/Insets.kt (82%) rename {insetter/src/main/java/dev/chrisbanes/accompanist/insetter => insets/src/main/java/dev/chrisbanes/accompanist/insets}/Padding.kt (93%) rename {insetter/src/main/java/dev/chrisbanes/accompanist/insetter => insets/src/main/java/dev/chrisbanes/accompanist/insets}/Size.kt (97%) delete mode 100644 insetter/gradle.properties diff --git a/README.md b/README.md index 34bd2db12..3456c4c78 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A number of libraries which aim to integrate some popular image loading librarie - 🖼️ [Picasso image loading composables](./picasso/) - 🖼️ [Glide image loading composables](./glide/) -### 📐 [Insetter](./insetter/) +### 📐 [Insets](./insets/) TODO --- diff --git a/generate_docs.sh b/generate_docs.sh index f4d984826..080ed115a 100755 --- a/generate_docs.sh +++ b/generate_docs.sh @@ -46,8 +46,8 @@ mkdir -p $DOCS_ROOT/glide cp glide/images/crossfade.gif $DOCS_ROOT/glide/crossfade.gif sed -i.bak 's/images\/crossfade.gif/crossfade.gif/' $DOCS_ROOT/glide.md -cp insetter/README.md $DOCS_ROOT/insetter.md -mkdir -p $DOCS_ROOT/insetter +cp insets/README.md $DOCS_ROOT/insets.md +mkdir -p $DOCS_ROOT/insets ######################### # Tidy up Dokka output diff --git a/insetter/README.md b/insets/README.md similarity index 57% rename from insetter/README.md rename to insets/README.md index 86fc2ee26..65e261038 100644 --- a/insetter/README.md +++ b/insets/README.md @@ -1,33 +1,33 @@ -# Insetter for Jetpack Compose +# Insets for Jetpack Compose -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.chrisbanes.accompanist/accompanist-insetter/badge.svg)](https://search.maven.org/search?q=g:dev.chrisbanes.accompanist) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.chrisbanes.accompanist/accompanist-insets/badge.svg)](https://search.maven.org/search?q=g:dev.chrisbanes.accompanist) -Insetter for Jetpack Compose takes a lot of the ideas which drove [Insetter][insetter-view] for views, and applies them for use in composables. +Insets for Jetpack Compose takes a lot of the ideas which drove [Insetter][insetter-view] for views, and applies them for use in composables. ## Usage -To setup Insetter in your composables, you need to call the `ProvideDisplayInsets` function and +To setup Insets in your composables, you need to call the `ProvideWindowInsets` function and wrap your content. This would typically be done near the top level of your composable hierarchy: ``` kotlin setContent { MaterialTheme { - ProvideDisplayInsets { + ProvideWindowInsets { // your content } } } ``` -> Note: Whether `ProvideDisplayInsets` is called outside or within `MaterialTheme` doesn't particularly matter. +> Note: Whether `ProvideWindowInsets` is called outside or within `MaterialTheme` doesn't particularly matter. -`ProvideDisplayInsets` allows Insetter to set an [`OnApplyWindowInsetsListener`][insetslistener] on your content's host view. That listener is used to update the value of an ambient bundled in this library: `AmbientInsetter`. +`ProvideWindowInsets` allows the library to set an [`OnApplyWindowInsetsListener`][insetslistener] on your content's host view. That listener is used to update the value of an ambient bundled in this library: `AmbientWindowInsets`. -`AmbientInsetter` holds an instance of `DisplayInsets` which contains the value of various [WindowInsets][insets] [types][insettypes]. You can use the values manually like so: +`AmbientWindowInsets` holds an instance of `WindowInsets` which contains the value of various [WindowInsets][insets] [types][insettypes]. You can use the values manually like so: ``` kotlin @Composable fun ImeAvoidingBox() { - val insets = AmbientInsetter.current + val insets = AmbientWindowInsets.current Box(Modifier.padding(bottom = insets.ime.bottom)) } @@ -48,7 +48,7 @@ repositories { } dependencies { - implementation "dev.chrisbanes.accompanist:accompanist-insetter:" + implementation "dev.chrisbanes.accompanist:accompanist-insets:" } ``` diff --git a/insetter/api/insetter.api b/insets/api/insets.api similarity index 65% rename from insetter/api/insetter.api rename to insets/api/insets.api index a5e2a5130..19aed62d6 100644 --- a/insetter/api/insetter.api +++ b/insets/api/insets.api @@ -1,20 +1,30 @@ -public final class dev/chrisbanes/accompanist/insetter/DisplayInsets { - public fun ()V - public final fun getIme ()Ldev/chrisbanes/accompanist/insetter/Insets; - public final fun getNavigationBars ()Ldev/chrisbanes/accompanist/insetter/Insets; - public final fun getStatusBars ()Ldev/chrisbanes/accompanist/insetter/Insets; - public final fun getSystemBars ()Ldev/chrisbanes/accompanist/insetter/Insets; - public final fun getSystemGestures ()Ldev/chrisbanes/accompanist/insetter/Insets; +public final class dev/chrisbanes/accompanist/insets/ComposeInsets { + public static final fun ProvideWindowInsets (ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun getAmbientWindowInsets ()Landroidx/compose/runtime/ProvidableAmbient; + public static final fun navigationBarWidth (Landroidx/compose/ui/Modifier;Ldev/chrisbanes/accompanist/insets/HorizontalSide;)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsHeight (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsHeightPlus-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsPadding (Landroidx/compose/ui/Modifier;ZZZ)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsPadding$default (Landroidx/compose/ui/Modifier;ZZZILjava/lang/Object;)Landroidx/compose/ui/Modifier; + public static final fun navigationBarsWidthPlus-UTaBBDU (Landroidx/compose/ui/Modifier;Ldev/chrisbanes/accompanist/insets/HorizontalSide;F)Landroidx/compose/ui/Modifier; + public static final fun statusBarsHeight (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + public static final fun statusBarsHeight-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; + public static final fun statusBarsHeight-wxomhCo$default (Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier; + public static final fun statusBarsHeightPlus-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; + public static final fun statusBarsPadding (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + public static final fun systemBarsPadding (Landroidx/compose/ui/Modifier;Z)Landroidx/compose/ui/Modifier; + public static final fun systemBarsPadding$default (Landroidx/compose/ui/Modifier;ZILjava/lang/Object;)Landroidx/compose/ui/Modifier; + public static final fun toPaddingValues (Ldev/chrisbanes/accompanist/insets/Insets;ZZZZLandroidx/compose/runtime/Composer;II)Landroidx/compose/foundation/layout/PaddingValues; } -public final class dev/chrisbanes/accompanist/insetter/HorizontalSide : java/lang/Enum { - public static final field Left Ldev/chrisbanes/accompanist/insetter/HorizontalSide; - public static final field Right Ldev/chrisbanes/accompanist/insetter/HorizontalSide; - public static final fun valueOf (Ljava/lang/String;)Ldev/chrisbanes/accompanist/insetter/HorizontalSide; - public static final fun values ()[Ldev/chrisbanes/accompanist/insetter/HorizontalSide; +public final class dev/chrisbanes/accompanist/insets/HorizontalSide : java/lang/Enum { + public static final field Left Ldev/chrisbanes/accompanist/insets/HorizontalSide; + public static final field Right Ldev/chrisbanes/accompanist/insets/HorizontalSide; + public static final fun valueOf (Ljava/lang/String;)Ldev/chrisbanes/accompanist/insets/HorizontalSide; + public static final fun values ()[Ldev/chrisbanes/accompanist/insets/HorizontalSide; } -public final class dev/chrisbanes/accompanist/insetter/Insets { +public final class dev/chrisbanes/accompanist/insets/Insets { public fun ()V public final fun getBottom ()I public final fun getLeft ()I @@ -23,29 +33,19 @@ public final class dev/chrisbanes/accompanist/insetter/Insets { public final fun isVisible ()Z } -public final class dev/chrisbanes/accompanist/insetter/Insetter { - public static final fun ProvideDisplayInsets (ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V - public static final fun getAmbientInsets ()Landroidx/compose/runtime/ProvidableAmbient; - public static final fun navigationBarWidth (Landroidx/compose/ui/Modifier;Ldev/chrisbanes/accompanist/insetter/HorizontalSide;)Landroidx/compose/ui/Modifier; - public static final fun navigationBarsHeight (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; - public static final fun navigationBarsHeightPlus-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; - public static final fun navigationBarsPadding (Landroidx/compose/ui/Modifier;ZZZ)Landroidx/compose/ui/Modifier; - public static final fun navigationBarsPadding$default (Landroidx/compose/ui/Modifier;ZZZILjava/lang/Object;)Landroidx/compose/ui/Modifier; - public static final fun navigationBarsWidthPlus-gYsdZ8Q (Landroidx/compose/ui/Modifier;Ldev/chrisbanes/accompanist/insetter/HorizontalSide;F)Landroidx/compose/ui/Modifier; - public static final fun statusBarsHeight (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; - public static final fun statusBarsHeight-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; - public static final fun statusBarsHeight-wxomhCo$default (Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier; - public static final fun statusBarsHeightPlus-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; - public static final fun statusBarsPadding (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; - public static final fun systemBarsPadding (Landroidx/compose/ui/Modifier;Z)Landroidx/compose/ui/Modifier; - public static final fun systemBarsPadding$default (Landroidx/compose/ui/Modifier;ZILjava/lang/Object;)Landroidx/compose/ui/Modifier; - public static final fun toPaddingValues (Ldev/chrisbanes/accompanist/insetter/Insets;ZZZZLandroidx/compose/runtime/Composer;II)Landroidx/compose/foundation/layout/PaddingValues; +public final class dev/chrisbanes/accompanist/insets/VerticalSide : java/lang/Enum { + public static final field Bottom Ldev/chrisbanes/accompanist/insets/VerticalSide; + public static final field Top Ldev/chrisbanes/accompanist/insets/VerticalSide; + public static final fun valueOf (Ljava/lang/String;)Ldev/chrisbanes/accompanist/insets/VerticalSide; + public static final fun values ()[Ldev/chrisbanes/accompanist/insets/VerticalSide; } -public final class dev/chrisbanes/accompanist/insetter/VerticalSide : java/lang/Enum { - public static final field Bottom Ldev/chrisbanes/accompanist/insetter/VerticalSide; - public static final field Top Ldev/chrisbanes/accompanist/insetter/VerticalSide; - public static final fun valueOf (Ljava/lang/String;)Ldev/chrisbanes/accompanist/insetter/VerticalSide; - public static final fun values ()[Ldev/chrisbanes/accompanist/insetter/VerticalSide; +public final class dev/chrisbanes/accompanist/insets/WindowInsets { + public fun ()V + public final fun getIme ()Ldev/chrisbanes/accompanist/insets/Insets; + public final fun getNavigationBars ()Ldev/chrisbanes/accompanist/insets/Insets; + public final fun getStatusBars ()Ldev/chrisbanes/accompanist/insets/Insets; + public final fun getSystemBars ()Ldev/chrisbanes/accompanist/insets/Insets; + public final fun getSystemGestures ()Ldev/chrisbanes/accompanist/insets/Insets; } diff --git a/insetter/build.gradle b/insets/build.gradle similarity index 100% rename from insetter/build.gradle rename to insets/build.gradle diff --git a/insets/gradle.properties b/insets/gradle.properties new file mode 100644 index 000000000..214b56a69 --- /dev/null +++ b/insets/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=accompanist-insets +POM_NAME=Accompanist Insets library +POM_PACKAGING=aar \ No newline at end of file diff --git a/insetter/src/androidTest/AndroidManifest.xml b/insets/src/androidTest/AndroidManifest.xml similarity index 93% rename from insetter/src/androidTest/AndroidManifest.xml rename to insets/src/androidTest/AndroidManifest.xml index c392b2f51..3b35fcff5 100644 --- a/insetter/src/androidTest/AndroidManifest.xml +++ b/insets/src/androidTest/AndroidManifest.xml @@ -15,7 +15,7 @@ --> + package="dev.chrisbanes.accompanist.insets.test"> diff --git a/insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt b/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTest.kt similarity index 93% rename from insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt rename to insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTest.kt index 89748c7b2..ccdd91bbf 100644 --- a/insetter/src/androidTest/java/dev/chrisbanes/accompanist/insetter/InsetterTest.kt +++ b/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package dev.chrisbanes.accompanist.insetter +package dev.chrisbanes.accompanist.insets import androidx.test.filters.LargeTest import androidx.ui.test.createComposeRule @@ -24,7 +24,7 @@ import org.junit.runners.JUnit4 @LargeTest @RunWith(JUnit4::class) -class InsetterTest { +class InsetsTest { @get:Rule val composeTestRule = createComposeRule() } diff --git a/insetter/src/main/AndroidManifest.xml b/insets/src/main/AndroidManifest.xml similarity index 91% rename from insetter/src/main/AndroidManifest.xml rename to insets/src/main/AndroidManifest.xml index 4b992b501..9ec81546e 100644 --- a/insetter/src/main/AndroidManifest.xml +++ b/insets/src/main/AndroidManifest.xml @@ -14,5 +14,5 @@ ~ limitations under the License. --> - + diff --git a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Insets.kt similarity index 82% rename from insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt rename to insets/src/main/java/dev/chrisbanes/accompanist/insets/Insets.kt index 6053bd5be..3b6b81108 100644 --- a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Insetter.kt +++ b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Insets.kt @@ -16,10 +16,10 @@ @file:Suppress("NOTHING_TO_INLINE", "unused") -@file:JvmName("Insetter") +@file:JvmName("ComposeInsets") @file:JvmMultifileClass -package dev.chrisbanes.accompanist.insetter +package dev.chrisbanes.accompanist.insets import android.view.View import androidx.compose.runtime.Composable @@ -40,7 +40,7 @@ import androidx.core.view.WindowInsetsCompat.Type * Main holder of our inset values. */ @Stable -class DisplayInsets { +class WindowInsets { /** * Inset values which match [WindowInsetsCompat.Type.systemBars] */ @@ -100,35 +100,35 @@ class Insets { internal set } -val AmbientInsets = staticAmbientOf { - error("AmbientInsets value not available. Are you using ProvideDisplayInsets?") +val AmbientWindowInsets = staticAmbientOf { + error("AmbientInsets value not available. Are you using ProvideWindowInsets?") } /** - * Applies any [WindowInsetsCompat] values to [AmbientInsets], which are then available + * Applies any [WindowInsetsCompat] values to [AmbientWindowInsets], which are then available * within [content]. * * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are dispatched to * the host view. Defaults to `true`. */ @Composable -fun ProvideDisplayInsets( +fun ProvideWindowInsets( consumeWindowInsets: Boolean = true, content: @Composable () -> Unit ) { val view = ViewAmbient.current - val displayInsets = remember { DisplayInsets() } + val windowInsets = remember { WindowInsets() } onCommit(view) { - ViewCompat.setOnApplyWindowInsetsListener(view) { _, windowInsets -> - displayInsets.systemBars.updateFrom(windowInsets, Type.systemBars()) - displayInsets.systemGestures.updateFrom(windowInsets, Type.systemGestures()) - displayInsets.statusBars.updateFrom(windowInsets, Type.statusBars()) - displayInsets.navigationBars.updateFrom(windowInsets, Type.navigationBars()) - displayInsets.ime.updateFrom(windowInsets, Type.ime()) - - if (consumeWindowInsets) WindowInsetsCompat.CONSUMED else windowInsets + ViewCompat.setOnApplyWindowInsetsListener(view) { _, wic -> + windowInsets.systemBars.updateFrom(wic, Type.systemBars()) + windowInsets.systemGestures.updateFrom(wic, Type.systemGestures()) + windowInsets.statusBars.updateFrom(wic, Type.statusBars()) + windowInsets.navigationBars.updateFrom(wic, Type.navigationBars()) + windowInsets.ime.updateFrom(wic, Type.ime()) + + if (consumeWindowInsets) WindowInsetsCompat.CONSUMED else wic } // Add an OnAttachStateChangeListener to request an inset pass each time we're attached @@ -149,7 +149,7 @@ fun ProvideDisplayInsets( } } - Providers(AmbientInsets provides displayInsets) { + Providers(AmbientWindowInsets provides windowInsets) { content() } } diff --git a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Padding.kt b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt similarity index 93% rename from insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Padding.kt rename to insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt index f3bb3cfc4..3c0b3e2ab 100644 --- a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Padding.kt +++ b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt @@ -16,10 +16,10 @@ @file:Suppress("NOTHING_TO_INLINE", "unused") -@file:JvmName("Insetter") +@file:JvmName("ComposeInsets") @file:JvmMultifileClass -package dev.chrisbanes.accompanist.insetter +package dev.chrisbanes.accompanist.insets import androidx.compose.ui.LayoutModifier import androidx.compose.ui.Measurable @@ -38,7 +38,7 @@ import androidx.compose.ui.unit.offset */ fun Modifier.systemBarsPadding(enabled: Boolean = true) = composed { insetsPadding( - insets = AmbientInsets.current.systemBars, + insets = AmbientWindowInsets.current.systemBars, left = enabled, top = enabled, right = enabled, @@ -51,7 +51,7 @@ fun Modifier.systemBarsPadding(enabled: Boolean = true) = composed { * of the content. */ fun Modifier.statusBarsPadding() = composed { - insetsPadding(insets = AmbientInsets.current.statusBars, top = true) + insetsPadding(insets = AmbientWindowInsets.current.statusBars, top = true) } /** @@ -72,7 +72,7 @@ fun Modifier.navigationBarsPadding( right: Boolean = true ) = composed { insetsPadding( - insets = AmbientInsets.current.navigationBars, + insets = AmbientWindowInsets.current.navigationBars, left = left, right = right, bottom = bottom diff --git a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Size.kt b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Size.kt similarity index 97% rename from insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Size.kt rename to insets/src/main/java/dev/chrisbanes/accompanist/insets/Size.kt index c2cf911bf..5591a9de9 100644 --- a/insetter/src/main/java/dev/chrisbanes/accompanist/insetter/Size.kt +++ b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Size.kt @@ -16,10 +16,10 @@ @file:Suppress("NOTHING_TO_INLINE", "unused") -@file:JvmName("Insetter") +@file:JvmName("ComposeInsets") @file:JvmMultifileClass -package dev.chrisbanes.accompanist.insetter +package dev.chrisbanes.accompanist.insets import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.height @@ -66,7 +66,7 @@ import androidx.compose.ui.unit.dp */ fun Modifier.statusBarsHeight(additional: Dp = 0.dp) = composed { InsetsSizeModifier( - insets = AmbientInsets.current.statusBars, + insets = AmbientWindowInsets.current.statusBars, heightSide = VerticalSide.Top, additionalHeight = additional ) @@ -118,7 +118,7 @@ inline fun Modifier.statusBarsHeight() = statusBarsHeightPlus(0.dp) */ fun Modifier.statusBarsHeightPlus(additional: Dp) = composed { InsetsSizeModifier( - insets = AmbientInsets.current.statusBars, + insets = AmbientWindowInsets.current.statusBars, heightSide = VerticalSide.Top, additionalHeight = additional ) @@ -170,7 +170,7 @@ inline fun Modifier.navigationBarsHeight() = navigationBarsHeightPlus(0.dp) */ fun Modifier.navigationBarsHeightPlus(additional: Dp) = composed { InsetsSizeModifier( - insets = AmbientInsets.current.navigationBars, + insets = AmbientWindowInsets.current.navigationBars, heightSide = VerticalSide.Bottom, additionalHeight = additional ) @@ -221,7 +221,7 @@ fun Modifier.navigationBarsWidthPlus( additional: Dp ) = composed { InsetsSizeModifier( - insets = AmbientInsets.current.navigationBars, + insets = AmbientWindowInsets.current.navigationBars, widthSide = side, additionalWidth = additional ) diff --git a/insetter/gradle.properties b/insetter/gradle.properties deleted file mode 100644 index ecd37ad0a..000000000 --- a/insetter/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_ARTIFACT_ID=accompanist-insetter -POM_NAME=Accompanist Insetter library -POM_PACKAGING=aar \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index cb55bd5a5..a1c4443d2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -30,5 +30,5 @@ include ':picasso' include ':glide' include ':imageloading-core' include ':imageloading-testutils' -include ':insetter' +include ':insets' include ':sample' From a1d0124d4b3e0e77abcae0a027a51cd5e1f97bb9 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 09:25:57 +0000 Subject: [PATCH 06/22] Simplify size modifier structure --- .idea/codeStyles/Project.xml | 4 +- insets/api/insets.api | 10 +- .../chrisbanes/accompanist/insets/Padding.kt | 10 +- .../dev/chrisbanes/accompanist/insets/Size.kt | 95 ++----------------- 4 files changed, 21 insertions(+), 98 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 62924bd05..6536f576a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -13,8 +13,8 @@ - \ No newline at end of file diff --git a/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/InsetsBasicSample.kt b/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/InsetsBasicSample.kt new file mode 100644 index 000000000..63caa392a --- /dev/null +++ b/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/InsetsBasicSample.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2020 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 dev.chrisbanes.accompanist.sample.insets + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.compose.foundation.Text +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Face +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.setContent +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat +import dev.chrisbanes.accompanist.insets.ProvideWindowInsets +import dev.chrisbanes.accompanist.insets.navigationBarsPadding +import dev.chrisbanes.accompanist.insets.statusBarsPadding +import dev.chrisbanes.accompanist.sample.R + +class InsetsBasicSample : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Turn off the decor fitting system windows, which means we need to through handling + // insets + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { + MaterialTheme { + Sample() + } + } + } +} + +@Composable +private fun Sample() { + ProvideWindowInsets { + Box(Modifier.fillMaxSize()) { + TopAppBar( + title = { + Text(stringResource(R.string.insets_title_basic)) + }, + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + .statusBarsPadding() + ) + + FloatingActionButton( + onClick = { /* */ }, + icon = { Icon(Icons.Default.Face) }, + modifier = Modifier.align(Alignment.BottomEnd) + .navigationBarsPadding() + .padding(16.dp) + ) + } + } +} diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index a128d118a..fba187420 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -28,4 +28,6 @@ Glide: Basic Glide: Grid Glide: Lazy row + + Insets: Basic diff --git a/sample/src/main/res/values/themes.xml b/sample/src/main/res/values/themes.xml index 41470d66a..ac92654d6 100644 --- a/sample/src/main/res/values/themes.xml +++ b/sample/src/main/res/values/themes.xml @@ -26,4 +26,12 @@ true + + From aea16eb1fbfd2eb085aeba7dc2e8893d6905ea96 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 10:51:53 +0000 Subject: [PATCH 10/22] Move to DisposableEffect --- .../src/main/java/dev/chrisbanes/accompanist/insets/Insets.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/insets/src/main/java/dev/chrisbanes/accompanist/insets/Insets.kt b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Insets.kt index 3b6b81108..f6e54c016 100644 --- a/insets/src/main/java/dev/chrisbanes/accompanist/insets/Insets.kt +++ b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Insets.kt @@ -23,11 +23,11 @@ package dev.chrisbanes.accompanist.insets import android.view.View import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Providers import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.onCommit import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.staticAmbientOf @@ -120,7 +120,7 @@ fun ProvideWindowInsets( val windowInsets = remember { WindowInsets() } - onCommit(view) { + DisposableEffect(view) { ViewCompat.setOnApplyWindowInsetsListener(view) { _, wic -> windowInsets.systemBars.updateFrom(wic, Type.systemBars()) windowInsets.systemGestures.updateFrom(wic, Type.systemGestures()) From bcf0e8138c53b8fcec59652667639fd87d4b2acc Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 10:53:21 +0000 Subject: [PATCH 11/22] Move Insets.toPaddingValues to Padding.kt --- .../chrisbanes/accompanist/insets/Padding.kt | 44 +++++++++++++++++++ .../dev/chrisbanes/accompanist/insets/Size.kt | 43 ------------------ 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt index d5b5c7cc0..3ec3d4229 100644 --- a/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt +++ b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt @@ -21,12 +21,18 @@ package dev.chrisbanes.accompanist.insets +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable import androidx.compose.ui.LayoutModifier import androidx.compose.ui.Measurable import androidx.compose.ui.MeasureScope import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.platform.DensityAmbient +import androidx.compose.ui.platform.LayoutDirectionAmbient import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset /** @@ -121,3 +127,41 @@ private data class InsetsPaddingModifier( } } } + +/** + * Returns the current insets converted into a [PaddingValues]. + * + * @param start Whether to apply the inset on the start dimension. + * @param top Whether to apply the inset on the top dimension. + * @param end Whether to apply the inset on the end dimension. + * @param bottom Whether to apply the inset on the bottom dimension. + */ +@Composable +fun Insets.toPaddingValues( + start: Boolean = true, + top: Boolean = true, + end: Boolean = true, + bottom: Boolean = true +): PaddingValues = with(DensityAmbient.current) { + val layoutDirection = LayoutDirectionAmbient.current + PaddingValues( + start = when { + start && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.left.toDp() + start && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.right.toDp() + else -> 0.dp + }, + top = when { + top -> this@toPaddingValues.top.toDp() + else -> 0.dp + }, + end = when { + end && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.right.toDp() + end && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.left.toDp() + else -> 0.dp + }, + bottom = when { + bottom -> this@toPaddingValues.bottom.toDp() + else -> 0.dp + } + ) +} diff --git a/insets/src/main/java/dev/chrisbanes/accompanist/insets/Size.kt b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Size.kt index c478c585f..409d90348 100644 --- a/insets/src/main/java/dev/chrisbanes/accompanist/insets/Size.kt +++ b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Size.kt @@ -21,9 +21,7 @@ package dev.chrisbanes.accompanist.insets -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable import androidx.compose.ui.LayoutModifier import androidx.compose.ui.Measurable import androidx.compose.ui.MeasureScope @@ -31,12 +29,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.layout.IntrinsicMeasurable import androidx.compose.ui.layout.IntrinsicMeasureScope -import androidx.compose.ui.platform.DensityAmbient -import androidx.compose.ui.platform.LayoutDirectionAmbient import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp /** @@ -150,44 +145,6 @@ fun Modifier.navigationBarsWidth( ) } -/** - * Returns the current insets converted into a [PaddingValues]. - * - * @param start Whether to apply the inset on the start dimension. - * @param top Whether to apply the inset on the top dimension. - * @param end Whether to apply the inset on the end dimension. - * @param bottom Whether to apply the inset on the bottom dimension. - */ -@Composable -fun Insets.toPaddingValues( - start: Boolean = true, - top: Boolean = true, - end: Boolean = true, - bottom: Boolean = true -): PaddingValues = with(DensityAmbient.current) { - val layoutDirection = LayoutDirectionAmbient.current - PaddingValues( - start = when { - start && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.left.toDp() - start && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.right.toDp() - else -> 0.dp - }, - top = when { - top -> this@toPaddingValues.top.toDp() - else -> 0.dp - }, - end = when { - end && layoutDirection == LayoutDirection.Ltr -> this@toPaddingValues.right.toDp() - end && layoutDirection == LayoutDirection.Rtl -> this@toPaddingValues.left.toDp() - else -> 0.dp - }, - bottom = when { - bottom -> this@toPaddingValues.bottom.toDp() - else -> 0.dp - } - ) -} - private data class InsetsSizeModifier( private val insets: Insets, private val widthSide: HorizontalSide? = null, From 9b53c9193ad438e48fdd8388c209673af276e839 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 10:55:29 +0000 Subject: [PATCH 12/22] Add comment InsetsSizeModifier --- .idea/codeStyles/Project.xml | 10 +++++++--- .../java/dev/chrisbanes/accompanist/insets/Size.kt | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 6536f576a..a915c72d1 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,8 +1,12 @@ - + + diff --git a/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsAssertions.kt b/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsAssertions.kt new file mode 100644 index 000000000..961482887 --- /dev/null +++ b/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsAssertions.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 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 dev.chrisbanes.accompanist.insets + +import android.graphics.Rect +import androidx.core.view.WindowInsetsCompat +import com.google.common.truth.Truth + +internal fun WindowInsets.assertEqualTo(insets: androidx.core.view.WindowInsetsCompat) { + systemBars.assertEqualTo( + insets = insets.getInsets(WindowInsetsCompat.Type.systemBars()), + visible = insets.isVisible(WindowInsetsCompat.Type.systemBars()), + ) + + statusBars.assertEqualTo( + insets = insets.getInsets(WindowInsetsCompat.Type.statusBars()), + visible = insets.isVisible(WindowInsetsCompat.Type.statusBars()), + ) + + navigationBars.assertEqualTo( + insets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()), + visible = insets.isVisible(WindowInsetsCompat.Type.navigationBars()), + ) + + ime.assertEqualTo( + insets = insets.getInsets(WindowInsetsCompat.Type.ime()), + visible = insets.isVisible(WindowInsetsCompat.Type.ime()), + ) +} + +internal fun Insets.assertEqualTo(insets: androidx.core.graphics.Insets, visible: Boolean) { + // This might look a bit weird, why are we using a Rect? Well, it makes the assertion + // error message much easier to read, by containing all of the dimensions. + Truth.assertThat(Rect(left, top, right, bottom)) + .isEqualTo(Rect(insets.left, insets.top, insets.right, insets.bottom)) + Truth.assertThat(this.isVisible).isEqualTo(visible) +} diff --git a/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTest.kt b/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTest.kt index ccdd91bbf..a1d6537f9 100644 --- a/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTest.kt +++ b/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTest.kt @@ -16,9 +16,13 @@ package dev.chrisbanes.accompanist.insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.test.filters.LargeTest -import androidx.ui.test.createComposeRule +import androidx.test.filters.SdkSuppress +import androidx.ui.test.createAndroidComposeRule import org.junit.Rule +import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -26,5 +30,23 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class InsetsTest { @get:Rule - val composeTestRule = createComposeRule() + val composeTestRule = createAndroidComposeRule(InsetsTestActivity::class.java) + + @Test + @SdkSuppress(minSdkVersion = 23) // ViewCompat.getRootWindowInsets + fun assertValuesMatchViewInsets() { + lateinit var composeWindowInsets: WindowInsets + composeTestRule.setContent { + ProvideWindowInsets { + composeWindowInsets = AmbientWindowInsets.current + } + } + + lateinit var rootWindowInsets: WindowInsetsCompat + composeTestRule.activityRule.scenario.onActivity { + rootWindowInsets = ViewCompat.getRootWindowInsets(it.window.decorView)!! + } + + composeWindowInsets.assertEqualTo(rootWindowInsets) + } } diff --git a/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTestActivity.kt b/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTestActivity.kt new file mode 100644 index 000000000..a9152b9e2 --- /dev/null +++ b/insets/src/androidTest/java/dev/chrisbanes/accompanist/insets/InsetsTestActivity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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 dev.chrisbanes.accompanist.insets + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.core.view.WindowCompat + +/** + * [ComponentActivity] which automatically requests for the decor not to fit system windows. + */ +class InsetsTestActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + } +} From 6838606c994b7363aa8da91d619d14b027a94b12 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 13:12:21 +0000 Subject: [PATCH 16/22] Add edge-to-edge lazy list sample --- .../chrisbanes/accompanist/insets/Padding.kt | 21 ++ sample/src/main/AndroidManifest.xml | 10 + .../sample/insets/EdgeToEdgeLazyColumn.kt | 188 ++++++++++++++++++ sample/src/main/res/values/strings.xml | 1 + 4 files changed, 220 insertions(+) create mode 100644 sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt diff --git a/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt index 3ec3d4229..a28407817 100644 --- a/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt +++ b/insets/src/main/java/dev/chrisbanes/accompanist/insets/Padding.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.composed import androidx.compose.ui.platform.DensityAmbient import androidx.compose.ui.platform.LayoutDirectionAmbient import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset @@ -165,3 +166,23 @@ fun Insets.toPaddingValues( } ) } + +/** + * Returns a new [PaddingValues] with the provided values added to each relevant dimension. + * + * @param start Value to add to the start dimension. + * @param top Value to add to the top dimension. + * @param end Value to add to the end dimension. + * @param bottom Value to add to the bottom dimension. + */ +inline fun PaddingValues.add( + start: Dp = 0.dp, + top: Dp = 0.dp, + end: Dp = 0.dp, + bottom: Dp = 0.dp, +): PaddingValues = copy( + start = this.start + start, + top = this.top + top, + end = this.end + end, + bottom = this.bottom + bottom +) diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index ce0e9f7f6..bed416b20 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -131,6 +131,16 @@ + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt b/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt new file mode 100644 index 000000000..fe18812dc --- /dev/null +++ b/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2020 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 dev.chrisbanes.accompanist.sample.insets + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.compose.foundation.Text +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.preferredSize +import androidx.compose.foundation.layout.preferredWidth +import androidx.compose.foundation.lazy.LazyColumnFor +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.TopAppBar +import androidx.compose.material.contentColorFor +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Face +import androidx.compose.material.primarySurface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.onSizeChanged +import androidx.compose.ui.platform.DensityAmbient +import androidx.compose.ui.platform.setContent +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat +import dev.chrisbanes.accompanist.glide.GlideImage +import dev.chrisbanes.accompanist.insets.AmbientWindowInsets +import dev.chrisbanes.accompanist.insets.ProvideWindowInsets +import dev.chrisbanes.accompanist.insets.add +import dev.chrisbanes.accompanist.insets.navigationBarsPadding +import dev.chrisbanes.accompanist.insets.statusBarsPadding +import dev.chrisbanes.accompanist.insets.toPaddingValues +import dev.chrisbanes.accompanist.sample.R +import dev.chrisbanes.accompanist.sample.randomSampleImageUrl + +class EdgeToEdgeLazyColumn : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Turn off the decor fitting system windows, which means we need to through handling + // insets + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { + MaterialTheme { + Sample() + } + } + } +} + +@OptIn(ExperimentalStdlibApi::class) +@Composable +private fun Sample() { + ProvideWindowInsets { + Surface { + Box(Modifier.fillMaxSize()) { + // A state instance which allows us to track the size of the top app bar + var topAppBarSize by remember { mutableStateOf(0) } + + LazyColumnFor( + items = listItems, + // We use the systemBar insets as the source of our content padding. + // We add on the topAppBarSize, so that the content is displayed below + // the app bar. Since the top inset is already contained within the app + // bar height, we disable handling it in toPaddingValues(). + contentPadding = AmbientWindowInsets.current.systemBars + .toPaddingValues(top = false) + .add(top = with(DensityAmbient.current) { topAppBarSize.toDp() }) + ) { imageUrl -> + ListItem(imageUrl, Modifier.fillMaxWidth()) + } + + InsetAwareTopAppBar( + title = { Text(stringResource(R.string.insets_title_list)) }, + modifier = Modifier.fillMaxWidth() + // We use onSizeChanged to track the app bar height, and update + // our state above + .onSizeChanged { topAppBarSize = it.height } + ) + + FloatingActionButton( + onClick = { /* TODO */ }, + icon = { Icon(Icons.Default.Face) }, + modifier = Modifier.align(Alignment.BottomEnd) + .navigationBarsPadding() + .padding(16.dp) + ) + } + } + } +} + +@OptIn(ExperimentalStdlibApi::class) +private val listItems = buildList { + repeat(40) { + add(randomSampleImageUrl(it)) + } +} + +/** + * A wrapper around [TopAppBar] which uses [Modifier.statusBarsPadding] to shift the app bar's + * contents down, but still draws the background behind the status bar too. + */ +@Composable +private fun InsetAwareTopAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable (() -> Unit)? = null, + actions: @Composable RowScope.() -> Unit = {}, + backgroundColor: Color = MaterialTheme.colors.primarySurface, + contentColor: Color = contentColorFor(backgroundColor), + elevation: Dp = 4.dp +) { + Surface( + color = backgroundColor, + elevation = elevation, + modifier = modifier + ) { + TopAppBar( + title = title, + navigationIcon = navigationIcon, + actions = actions, + backgroundColor = Color.Transparent, + contentColor = contentColor, + elevation = 0.dp, + modifier = Modifier.statusBarsPadding() + ) + } +} + +/** + * Simple list item row which displays an image and text. + */ +@Composable +private fun ListItem( + imageUrl: String, + modifier: Modifier = Modifier +) { + Row(modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + GlideImage( + data = imageUrl, + modifier = Modifier.preferredSize(64.dp) + .clip(RoundedCornerShape(4.dp)) + ) + + Spacer(Modifier.preferredWidth(16.dp)) + + Text( + text = "Text", + style = MaterialTheme.typography.subtitle2, + modifier = Modifier.weight(1f) + .align(Alignment.CenterVertically) + ) + } +} diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index fba187420..7e8b946ae 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -30,4 +30,5 @@ Glide: Lazy row Insets: Basic + Insets: Edge-to-edge list From 97e468f544bfe268194433a61ff6c462e7942ef1 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 13:13:36 +0000 Subject: [PATCH 17/22] Link to new sample --- insets/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/insets/README.md b/insets/README.md index fd7b1880a..0621b4d9c 100644 --- a/insets/README.md +++ b/insets/README.md @@ -87,6 +87,8 @@ LazyColumn( ) ``` +For a more complex example, see the [`EdgeToEdgeLazyColumn`](./sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt) example. + ## Download ```groovy From 5e3436ba26f14a4233413a7f3fa278162393df23 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 13:25:21 +0000 Subject: [PATCH 18/22] Fix API --- insets/api/insets.api | 2 ++ 1 file changed, 2 insertions(+) diff --git a/insets/api/insets.api b/insets/api/insets.api index 9cd275217..b34b90b88 100644 --- a/insets/api/insets.api +++ b/insets/api/insets.api @@ -1,5 +1,7 @@ public final class dev/chrisbanes/accompanist/insets/ComposeInsets { public static final fun ProvideWindowInsets (ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun add-L4cKpv0 (Landroidx/compose/foundation/layout/PaddingValues;FFFF)Landroidx/compose/foundation/layout/PaddingValues; + public static final fun add-L4cKpv0$default (Landroidx/compose/foundation/layout/PaddingValues;FFFFILjava/lang/Object;)Landroidx/compose/foundation/layout/PaddingValues; public static final fun getAmbientWindowInsets ()Landroidx/compose/runtime/ProvidableAmbient; public static final fun navigationBarsHeight-wxomhCo (Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; public static final fun navigationBarsHeight-wxomhCo$default (Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier; From 16d6a8022b9b4c3314982fbc9948c15d790ae13d Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 13:26:49 +0000 Subject: [PATCH 19/22] Expand README for insets --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3456c4c78..a0e4197e8 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,15 @@ Accompanist is a group of libraries that contains some utilities which I've found myself copying around projects which use [Jetpack Compose][compose]. Currently, it contains: ### Image loading -A number of libraries which aim to integrate some popular image loading libraries into Compose: +A number of libraries which integrate popular image loading libraries into Jetpack Compose: - 🖼️ [Coil image loading composables](./coil/) - 🖼️ [Picasso image loading composables](./picasso/) - 🖼️ [Glide image loading composables](./glide/) ### 📐 [Insets](./insets/) -TODO +A library which brings [WindowInsets](https://developer.android.com/reference/kotlin/android/view/WindowInsets) support to Jetpack Compose. + --- From 73b1817764ab34e898d6e7c100631d5b0f5309ae Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 15:02:54 +0000 Subject: [PATCH 20/22] Update Edge to Edge sample to use translucent app bar --- .../accompanist/sample/insets/EdgeToEdgeLazyColumn.kt | 5 +++++ sample/src/main/res/values/themes.xml | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt b/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt index fe18812dc..5ee40d9cf 100644 --- a/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt +++ b/sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt @@ -103,8 +103,13 @@ private fun Sample() { ListItem(imageUrl, Modifier.fillMaxWidth()) } + /** + * We show a translucent app bar above which floats about the content. Our + * [InsetAwareTopAppBar] below automatically draws behind the status bar too. + */ InsetAwareTopAppBar( title = { Text(stringResource(R.string.insets_title_list)) }, + backgroundColor = MaterialTheme.colors.surface.copy(alpha = 0.9f), modifier = Modifier.fillMaxWidth() // We use onSizeChanged to track the app bar height, and update // our state above diff --git a/sample/src/main/res/values/themes.xml b/sample/src/main/res/values/themes.xml index ac92654d6..e05c1f958 100644 --- a/sample/src/main/res/values/themes.xml +++ b/sample/src/main/res/values/themes.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> - + From c704af6b752fdfd7b89d9fbcbfe58514ec87cff9 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 15:30:28 +0000 Subject: [PATCH 21/22] Tweak website generation for insets --- generate_docs.sh | 10 ++++------ insets/README.md | 6 +++++- insets/images/edge-to-edge-list.jpg | Bin 0 -> 47792 bytes mkdocs.yml | 3 ++- 4 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 insets/images/edge-to-edge-list.jpg diff --git a/generate_docs.sh b/generate_docs.sh index 080ed115a..b3a042846 100755 --- a/generate_docs.sh +++ b/generate_docs.sh @@ -33,21 +33,19 @@ sed -i.bak 's/docs\/\([a-zA-Z-]*\).md/\1/' $DOCS_ROOT/index.md cp coil/README.md $DOCS_ROOT/coil.md mkdir -p $DOCS_ROOT/coil -cp coil/images/crossfade.gif $DOCS_ROOT/coil/crossfade.gif -sed -i.bak 's/images\/crossfade.gif/crossfade.gif/' $DOCS_ROOT/coil.md +cp -r coil/images $DOCS_ROOT/coil cp picasso/README.md $DOCS_ROOT/picasso.md mkdir -p $DOCS_ROOT/picasso -cp picasso/images/crossfade.gif $DOCS_ROOT/picasso/crossfade.gif -sed -i.bak 's/images\/crossfade.gif/crossfade.gif/' $DOCS_ROOT/picasso.md +cp -r picasso/images $DOCS_ROOT/picasso cp glide/README.md $DOCS_ROOT/glide.md mkdir -p $DOCS_ROOT/glide -cp glide/images/crossfade.gif $DOCS_ROOT/glide/crossfade.gif -sed -i.bak 's/images\/crossfade.gif/crossfade.gif/' $DOCS_ROOT/glide.md +cp -r glide/images $DOCS_ROOT/glide cp insets/README.md $DOCS_ROOT/insets.md mkdir -p $DOCS_ROOT/insets +cp -r insets/images $DOCS_ROOT/insets ######################### # Tidy up Dokka output diff --git a/insets/README.md b/insets/README.md index 0621b4d9c..d1405f4f4 100644 --- a/insets/README.md +++ b/insets/README.md @@ -87,7 +87,11 @@ LazyColumn( ) ``` -For a more complex example, see the [`EdgeToEdgeLazyColumn`](./sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt) example. +For a more complex example, see the [`EdgeToEdgeLazyColumn`](./sample/src/main/java/dev/chrisbanes/accompanist/sample/insets/EdgeToEdgeLazyColumn.kt) example: + + + + ## Download diff --git a/insets/images/edge-to-edge-list.jpg b/insets/images/edge-to-edge-list.jpg new file mode 100644 index 0000000000000000000000000000000000000000..789e7bdfd5cd7802f93cd552b2ac1056c6a62ded GIT binary patch literal 47792 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 literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index 26d3d64c5..01f92d4de 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,12 +17,13 @@ nav: - 'Coil': coil.md - 'Glide': glide.md - 'Picasso': picasso.md + - 'Insets': insets.md - 'API': - 'Coil': api/coil/index.md - 'Glide': api/glide/index.md - 'Picasso': api/picasso/index.md - 'Image Loading Core': api/imageloading-core/index.md - - 'Insetter': api/insetter/index.md + - 'Insets': api/insets/index.md - 'Snapshots': using-snapshot-version.md - 'Contributing': contributing.md - 'Maintainers': From 683fd236159a6b49befcebf10ae2b694aba5155f Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Fri, 30 Oct 2020 15:33:43 +0000 Subject: [PATCH 22/22] Update insets/README.md Co-authored-by: Nick Butcher --- insets/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/insets/README.md b/insets/README.md index d1405f4f4..a316d99cc 100644 --- a/insets/README.md +++ b/insets/README.md @@ -108,7 +108,7 @@ dependencies { Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit. [compose]: https://developer.android.com/jetpack/compose -[snap]: https://oss.sonatype.org/content/repositories/snapshots/dev/chrisbanes/accompanist/accompanist-glide/ +[snap]: https://oss.sonatype.org/content/repositories/snapshots/dev/chrisbanes/accompanist/accompanist-insets/ [insetter-view]: https://github.com/chrisbanes/insetter [insets]: https://developer.android.com/reference/kotlin/androidx/core/view/WindowInsetsCompat [insettypes]: https://developer.android.com/reference/kotlin/androidx/core/view/WindowInsetsCompat.Type @@ -116,4 +116,4 @@ Snapshots of the development version are available in [Sonatype's `snapshots` re [modifier]: https://developer.android.com/reference/kotlin/androidx/ui/core/Modifier [paddingvalues]: https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/PaddingValues [lazycolumn]: https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/package-summary#lazycolumn -[fab]: https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#floatingactionbutton \ No newline at end of file +[fab]: https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#floatingactionbutton