Use correct fold size units and update content drawer logic (#80)
* Update content drawer to use custom separated column implementation * Update gradle version * Update content drawer and padding values * Rename params with units * Update UI tests * Add missing content descriptions to CompanionPane * Update dependencies * Remove unnecessary dependency * Remove redundant px/dp conversions * Update readme * Change to stable dependencies
This commit is contained in:
Родитель
289e116675
Коммит
f79c108632
|
@ -29,9 +29,12 @@ android {
|
|||
kotlinCompilerExtensionVersion composeVersion
|
||||
}
|
||||
packagingOptions {
|
||||
exclude "META-INF/licenses/**"
|
||||
exclude "META-INF/AL2.0"
|
||||
exclude "META-INF/LGPL2.1"
|
||||
jniLibs {
|
||||
excludes += ['META-INF/licenses/**']
|
||||
}
|
||||
resources {
|
||||
excludes += ['META-INF/licenses/**', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,17 +39,19 @@ enum class Filters(@StringRes val title: Int, @DrawableRes val image: Int) {
|
|||
LUDWIG(R.string.ludwig, R.drawable.ludwig)
|
||||
}
|
||||
|
||||
enum class Effects(@StringRes val title: Int, @DrawableRes val image: Int) {
|
||||
FILTER(R.string.filter, R.drawable.filter_icon),
|
||||
HDR(R.string.hdr, R.drawable.hdr_icon),
|
||||
ELLIPSE(R.string.ellipse, R.drawable.ecllipse_icon),
|
||||
HORIZONTAL(R.string.vertical, R.drawable.vertical_icon),
|
||||
VERTICAL(R.string.horizontal, R.drawable.horizontal_icon),
|
||||
ZOOM(R.string.zoom, R.drawable.zoom_icon),
|
||||
BRIGHTNESS(R.string.brightness, R.drawable.brightness_icon)
|
||||
}
|
||||
|
||||
private val filterList = Filters.values()
|
||||
|
||||
private val fullIconList = listOf<@DrawableRes Int>(
|
||||
R.drawable.filter_icon,
|
||||
R.drawable.hdr_icon,
|
||||
R.drawable.ecllipse_icon,
|
||||
R.drawable.vertical_icon,
|
||||
R.drawable.horizontal_icon,
|
||||
R.drawable.zoom_icon,
|
||||
R.drawable.brightness_icon
|
||||
)
|
||||
private val fullIconList = Effects.values().toList()
|
||||
|
||||
private val shortIconList = fullIconList.subList(2, 5)
|
||||
|
||||
|
@ -122,7 +124,7 @@ fun AdjustScale() {
|
|||
.height(5.dp),
|
||||
contentScale = ContentScale.Inside,
|
||||
alignment = Alignment.Center,
|
||||
contentDescription = null
|
||||
contentDescription = stringResource(R.string.dot)
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.scale_icon),
|
||||
|
@ -131,7 +133,7 @@ fun AdjustScale() {
|
|||
.height(25.dp),
|
||||
contentScale = ContentScale.Inside,
|
||||
alignment = Alignment.Center,
|
||||
contentDescription = null
|
||||
contentDescription = stringResource(R.string.scale)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -146,8 +148,8 @@ fun FullIconsPanel() {
|
|||
) {
|
||||
fullIconList.forEach { icon ->
|
||||
Image(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
painter = painterResource(id = icon.image),
|
||||
contentDescription = stringResource(icon.title),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -163,8 +165,8 @@ fun ShortIconsPanel() {
|
|||
) {
|
||||
shortIconList.forEach { icon ->
|
||||
Image(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
painter = painterResource(id = icon.image),
|
||||
contentDescription = stringResource(icon.title),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,14 @@
|
|||
<string name="definition">Definition</string>
|
||||
<string name="vignette">Vignette</string>
|
||||
<string name="brightness">Brightness</string>
|
||||
<string name="dot">Dot</string>
|
||||
<string name="scale">Scale</string>
|
||||
<string name="filter">Filter</string>
|
||||
<string name="hdr">HDR</string>
|
||||
<string name="ellipse">Ellipse</string>
|
||||
<string name="vertical">Vertical</string>
|
||||
<string name="horizontal">Horizontal</string>
|
||||
<string name="zoom">Zoom</string>
|
||||
|
||||
<!-- Filters -->
|
||||
<string name="filters">Filters</string>
|
||||
|
|
|
@ -29,9 +29,12 @@ android {
|
|||
kotlinCompilerExtensionVersion composeVersion
|
||||
}
|
||||
packagingOptions {
|
||||
exclude "META-INF/licenses/**"
|
||||
exclude "META-INF/AL2.0"
|
||||
exclude "META-INF/LGPL2.1"
|
||||
jniLibs {
|
||||
excludes += ['META-INF/licenses/**']
|
||||
}
|
||||
resources {
|
||||
excludes += ['META-INF/licenses/**', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,9 +29,12 @@ android {
|
|||
kotlinCompilerExtensionVersion composeVersion
|
||||
}
|
||||
packagingOptions {
|
||||
exclude "META-INF/licenses/**"
|
||||
exclude "META-INF/AL2.0"
|
||||
exclude "META-INF/LGPL2.1"
|
||||
jniLibs {
|
||||
excludes += ['META-INF/licenses/**']
|
||||
}
|
||||
resources {
|
||||
excludes += ['META-INF/licenses/**', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,9 +29,12 @@ android {
|
|||
kotlinCompilerExtensionVersion composeVersion
|
||||
}
|
||||
packagingOptions {
|
||||
exclude "META-INF/licenses/**"
|
||||
exclude "META-INF/AL2.0"
|
||||
exclude "META-INF/LGPL2.1"
|
||||
jniLibs {
|
||||
excludes += ['META-INF/licenses/**']
|
||||
}
|
||||
resources {
|
||||
excludes += ['META-INF/licenses/**', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ dependencies {
|
|||
implementation microsoftDependencies.twoPaneLayout
|
||||
implementation microsoftDependencies.windowState
|
||||
|
||||
implementation composeDependencies.composeMaterialForNavRail
|
||||
implementation composeDependencies.composeMaterial
|
||||
implementation composeDependencies.composeRuntime
|
||||
implementation composeDependencies.navigationCompose
|
||||
implementation composeDependencies.composeAnimation
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
|
||||
package com.microsoft.device.display.samples.navigationrail
|
||||
|
||||
import android.graphics.Rect
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.semantics.SemanticsProperties.HorizontalScrollAxisRange
|
||||
import androidx.compose.ui.semantics.SemanticsProperties.VerticalScrollAxisRange
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
|
@ -195,7 +197,9 @@ class DetailTest {
|
|||
Pane2(
|
||||
isDualPortrait = isDualPortrait,
|
||||
isDualLandscape = isDualLandscape,
|
||||
foldSize = 0.dp,
|
||||
foldOccludes = false,
|
||||
foldBounds = Rect(0, 0, 0, 0),
|
||||
windowHeight = LocalConfiguration.current.screenHeightDp.dp,
|
||||
imageId = 0,
|
||||
updateImageId = {},
|
||||
currentRoute = "plants"
|
||||
|
@ -212,7 +216,9 @@ class DetailTest {
|
|||
ItemDetailView(
|
||||
isDualPortrait = false,
|
||||
isDualLandscape = false,
|
||||
foldSize = 0.dp,
|
||||
foldOccludes = false,
|
||||
foldBounds = Rect(0, 0, 0, 0),
|
||||
windowHeight = LocalConfiguration.current.screenHeightDp.dp,
|
||||
selectedImage = plantList[0],
|
||||
currentRoute = "plants"
|
||||
)
|
||||
|
|
|
@ -5,20 +5,25 @@
|
|||
|
||||
package com.microsoft.device.display.samples.navigationrail.ui.components
|
||||
|
||||
import android.graphics.RectF
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.BottomSheetScaffoldDefaults
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.SwipeableState
|
||||
import androidx.compose.material.rememberSwipeableState
|
||||
import androidx.compose.material.swipeable
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -34,6 +39,7 @@ import androidx.compose.ui.semantics.semantics
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.microsoft.device.display.samples.navigationrail.R
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
private const val CONTENT_HORIZ_PADDING_PERECENT = 0.06f
|
||||
private val DrawerShape = RoundedCornerShape(
|
||||
|
@ -53,61 +59,83 @@ enum class DrawerState { Collapsed, Expanded }
|
|||
* Custom drawer (bottom aligned) with a rounded corner shape that swipes between a collapsed
|
||||
* "peek" view and a more expanded view that displays all of the content
|
||||
*
|
||||
* Supports foldable displays by splitting the "peekContent" and "hiddenContent" around an occluding
|
||||
* fold
|
||||
*
|
||||
* @param modifier: optional Modifier to be applied to the layout
|
||||
* @param expandHeight: height of the drawer when expanded (in dp)
|
||||
* @param collapseHeight: height of the drawer when collpased (in dp)
|
||||
* @param hingeOccludes: optional param for foldable support, indicates whether there is a hinge
|
||||
* @param expandedHeightPct: height of the drawer when expanded, expressed as percentage of maximum possible
|
||||
* height (must be > 0, <= 1)
|
||||
* @param collapsedHeightPct: height of the drawer when collapsed, expressed as percentage of maximum possible
|
||||
* height (must be > 0, <= 1)
|
||||
* @param foldOccludes: optional param for foldable support, indicates whether there is a hinge
|
||||
* that occludes content in the current layout
|
||||
* @param foldSize: optional param for foldable support, indicates the size of a fold
|
||||
* @param foldBoundsDp: optional param for foldable support, indicates the coordinates of the boundary
|
||||
* of a fold
|
||||
* @param windowHeightDp: optional param for foldable support, indicates the full height of the window
|
||||
* in which a fold and the content drawer are being displayed
|
||||
* @param foldBottomPaddingDp: optional param for foldable support, will be added as padding below the fold to
|
||||
* make content more accessible to users
|
||||
* @param hiddenContent: the content that will only be shown when the drawer is expanded
|
||||
* @param peekContent: the content that will be shown even when the drawer is collapsed
|
||||
*/
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun ContentDrawer(
|
||||
fun BoxWithConstraintsScope.ContentDrawer(
|
||||
modifier: Modifier = Modifier,
|
||||
expandHeight: Dp,
|
||||
collapseHeight: Dp,
|
||||
hingeOccludes: Boolean = false,
|
||||
foldSize: Dp = 0.dp,
|
||||
expandedHeightPct: Float,
|
||||
collapsedHeightPct: Float,
|
||||
foldOccludes: Boolean = false,
|
||||
foldBoundsDp: RectF = RectF(),
|
||||
windowHeightDp: Dp = 0.dp,
|
||||
foldBottomPaddingDp: Dp = 0.dp,
|
||||
hiddenContent: @Composable ColumnScope.() -> Unit,
|
||||
peekContent: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
// Calculate drawer y coordinates for the collapsed and expanded states
|
||||
val expandHeightPx = with(LocalDensity.current) { expandHeight.toPx() }
|
||||
val collapseHeightPx = with(LocalDensity.current) { collapseHeight.toPx() }
|
||||
// Calculate drawer y coordinates for the collapsed and expanded states - pixels
|
||||
if (!expandedHeightPct.isInPctRange() || !collapsedHeightPct.isInPctRange())
|
||||
throw IllegalArgumentException("expandedHeightPct $expandedHeightPct or collapsedHeightPct $collapsedHeightPct is not in range (0, 1]")
|
||||
val fullHeight = constraints.maxHeight.toFloat()
|
||||
val expandHeightPx = expandedHeightPct * fullHeight
|
||||
val collapseHeightPx = collapsedHeightPct * fullHeight
|
||||
val swipeHeightPx = expandHeightPx - collapseHeightPx
|
||||
|
||||
// Set up swipeable modifier fields
|
||||
val swipeableState = rememberSwipeableState(initialValue = DrawerState.Collapsed)
|
||||
val anchors = mapOf(swipeHeightPx to DrawerState.Collapsed, 0f to DrawerState.Expanded)
|
||||
|
||||
// Calculate the height of each drawer component (top content, fold, bottom content) - dp
|
||||
val expandHeightDp = with(LocalDensity.current) { expandHeightPx.toDp() }
|
||||
val collapseHeightDp = with(LocalDensity.current) { collapseHeightPx.toDp() }
|
||||
val foldSizeDp = foldBoundsDp.height().dp
|
||||
val bottomContentMaxHeightDp = windowHeightDp - foldBoundsDp.bottom.dp
|
||||
val topContentMaxHeightDp: Dp = if (foldOccludes) {
|
||||
expandHeightDp - foldSizeDp - bottomContentMaxHeightDp
|
||||
} else {
|
||||
collapseHeightDp
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.swipeable(swipeableState, anchors, Orientation.Vertical)
|
||||
.semantics { this.drawerState = swipeableState.currentValue }
|
||||
.testTag(stringResource(R.string.content_drawer)),
|
||||
contentAlignment = Alignment.TopStart,
|
||||
) {
|
||||
// Check if a spacer needs to be included to render content around an occluding hinge
|
||||
val spacerHeight = if (hingeOccludes) {
|
||||
val isExpanding = swipeableState.progress.to == DrawerState.Expanded
|
||||
val progressHeight = (foldSize.value * swipeableState.progress.fraction).dp
|
||||
if (isExpanding)
|
||||
progressHeight
|
||||
else
|
||||
foldSize - progressHeight
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
val minSpacerHeight = calculateSpacerHeight(
|
||||
foldOccludes,
|
||||
swipeableState,
|
||||
foldSizeDp.value + foldBottomPaddingDp.value
|
||||
).toInt().dp
|
||||
|
||||
// Calculate drawer height based on swipe state (height in dp)
|
||||
// Calculate drawer height in dp based on swipe state
|
||||
val swipeOffsetDp = with(LocalDensity.current) { swipeableState.offset.value.toDp() }
|
||||
val drawerHeight = expandHeight - swipeOffsetDp
|
||||
val drawerHeight = expandHeightDp - swipeOffsetDp
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.heightIn(collapseHeight, expandHeight)
|
||||
.heightIn(collapseHeightDp, expandHeightDp)
|
||||
.height(drawerHeight)
|
||||
.clip(DrawerShape)
|
||||
.background(MaterialTheme.colors.surface),
|
||||
|
@ -117,13 +145,48 @@ fun ContentDrawer(
|
|||
val paddingPx = CONTENT_HORIZ_PADDING_PERECENT * constraints.maxWidth.toFloat()
|
||||
val paddingDp = with(LocalDensity.current) { paddingPx.toDp() }
|
||||
|
||||
val fillWidth = Modifier.fillMaxWidth()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = paddingDp)
|
||||
modifier = Modifier.padding(horizontal = paddingDp),
|
||||
) {
|
||||
peekContent()
|
||||
Spacer(Modifier.height(spacerHeight))
|
||||
Column(fillWidth.requiredHeight(topContentMaxHeightDp)) { peekContent() }
|
||||
Spacer(Modifier.requiredHeight(minSpacerHeight))
|
||||
hiddenContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to calculate the animated height of the spacer used for foldable support. Height
|
||||
* is progressively increased or decreased based on the swipe state.
|
||||
*
|
||||
* @param foldOccludes: whether or not a fold is present and occluding content
|
||||
* @param swipeableState: swipeable state of the component that contains a spacer
|
||||
* @param fullHeight: the desired full height of the spacer when the parent component has been swiped
|
||||
* to the expanded state
|
||||
*
|
||||
* @return the height of the spacer for the current swipe progress
|
||||
*/
|
||||
@ExperimentalMaterialApi
|
||||
private fun calculateSpacerHeight(
|
||||
foldOccludes: Boolean,
|
||||
swipeableState: SwipeableState<DrawerState>,
|
||||
fullHeight: Float
|
||||
): Float {
|
||||
if (!foldOccludes)
|
||||
return 0f
|
||||
|
||||
val isExpanding = swipeableState.progress.to == DrawerState.Expanded
|
||||
val progressHeight = (fullHeight * swipeableState.progress.fraction)
|
||||
|
||||
return if (isExpanding) progressHeight else fullHeight - progressHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that checks if a percentage is valid
|
||||
*/
|
||||
private fun Float.isInPctRange(): Boolean {
|
||||
return this > 0f && this <= 1f
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package com.microsoft.device.display.samples.navigationrail.ui.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
|
@ -16,7 +17,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.ExperimentalUnitApi
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.microsoft.device.display.samples.navigationrail.models.DataProvider
|
||||
import com.microsoft.device.display.samples.navigationrail.ui.components.ItemTopBar
|
||||
import com.microsoft.device.dualscreen.twopanelayout.TwoPaneLayout
|
||||
|
@ -43,8 +43,9 @@ fun NavigationRailApp(windowState: WindowState) {
|
|||
isDualScreen = windowState.isDualScreen(),
|
||||
isDualPortrait = windowState.isDualPortrait(),
|
||||
isDualLandscape = windowState.isDualLandscape(),
|
||||
// REVISIT: fix when addressing https://github.com/microsoft/surface-duo-compose-samples/issues/74
|
||||
foldSize = windowState.foldSize.dp,
|
||||
foldOccludes = windowState.foldOccludes,
|
||||
foldBounds = windowState.foldBounds,
|
||||
windowHeight = windowState.windowHeight,
|
||||
imageId = imageId,
|
||||
updateImageId = updateImageId,
|
||||
currentRoute = currentRoute,
|
||||
|
@ -61,7 +62,9 @@ fun NavigationRailAppContent(
|
|||
isDualScreen: Boolean,
|
||||
isDualPortrait: Boolean,
|
||||
isDualLandscape: Boolean,
|
||||
foldSize: Dp,
|
||||
foldOccludes: Boolean,
|
||||
foldBounds: Rect,
|
||||
windowHeight: Dp,
|
||||
imageId: Int?,
|
||||
updateImageId: (Int?) -> Unit,
|
||||
currentRoute: String,
|
||||
|
@ -73,7 +76,16 @@ fun NavigationRailAppContent(
|
|||
Pane1(isDualScreen, isDualPortrait, imageId, updateImageId, currentRoute, updateRoute)
|
||||
},
|
||||
pane2 = {
|
||||
Pane2(isDualPortrait, isDualLandscape, foldSize, imageId, updateImageId, currentRoute)
|
||||
Pane2(
|
||||
isDualPortrait = isDualPortrait,
|
||||
isDualLandscape = isDualLandscape,
|
||||
foldOccludes = foldOccludes,
|
||||
foldBounds = foldBounds,
|
||||
windowHeight = windowHeight,
|
||||
imageId = imageId,
|
||||
updateImageId = updateImageId,
|
||||
currentRoute = currentRoute
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -107,7 +119,9 @@ fun Pane1(
|
|||
fun Pane2(
|
||||
isDualPortrait: Boolean,
|
||||
isDualLandscape: Boolean,
|
||||
foldSize: Dp,
|
||||
foldOccludes: Boolean,
|
||||
foldBounds: Rect,
|
||||
windowHeight: Dp,
|
||||
imageId: Int?,
|
||||
updateImageId: (Int?) -> Unit,
|
||||
currentRoute: String,
|
||||
|
@ -122,7 +136,15 @@ fun Pane2(
|
|||
}
|
||||
BackHandler { if (!isDualPortrait) onBackPressed() }
|
||||
|
||||
ItemDetailView(isDualPortrait, isDualLandscape, foldSize, selectedImage, currentRoute)
|
||||
ItemDetailView(
|
||||
isDualPortrait = isDualPortrait,
|
||||
isDualLandscape = isDualLandscape,
|
||||
foldOccludes = foldOccludes,
|
||||
foldBounds = foldBounds,
|
||||
windowHeight = windowHeight,
|
||||
selectedImage = selectedImage,
|
||||
currentRoute = currentRoute
|
||||
)
|
||||
// If only one pane is being displayed, show a "back" icon
|
||||
if (!isDualPortrait) {
|
||||
ItemTopBar(
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package com.microsoft.device.display.samples.navigationrail.ui.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
@ -28,7 +29,9 @@ import com.microsoft.device.dualscreen.twopanelayout.navigateToPane1
|
|||
*
|
||||
* @param isDualPortrait: true if device is in dual portrait mode
|
||||
* @param isDualLandscape: true if device is in dual landscape mode
|
||||
* @param foldSize: size of fold in dp (0 if no fold)
|
||||
* @param foldOccludes: true if a fold is present and it occludes content, false otherwise
|
||||
* @param foldBounds: the bounds of a fold in the form of an Android Rect
|
||||
* @param windowHeight: full height in dp of the window this view is being shown in
|
||||
* @param selectedImage: currently selected image
|
||||
* @param currentRoute: current route in gallery NavHost
|
||||
*/
|
||||
|
@ -38,7 +41,9 @@ import com.microsoft.device.dualscreen.twopanelayout.navigateToPane1
|
|||
fun ItemDetailView(
|
||||
isDualPortrait: Boolean,
|
||||
isDualLandscape: Boolean,
|
||||
foldSize: Dp,
|
||||
foldOccludes: Boolean,
|
||||
foldBounds: Rect,
|
||||
windowHeight: Dp,
|
||||
selectedImage: Image? = null,
|
||||
currentRoute: String,
|
||||
) {
|
||||
|
@ -60,10 +65,12 @@ fun ItemDetailView(
|
|||
) {
|
||||
ItemImage(Modifier.align(Alignment.TopCenter), selectedImage)
|
||||
ItemDetailsDrawer(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
image = selectedImage,
|
||||
isDualLandscape = isDualLandscape,
|
||||
foldSize = foldSize,
|
||||
isDualPortrait = isDualPortrait,
|
||||
foldOccludes = foldOccludes,
|
||||
foldBounds = foldBounds,
|
||||
windowHeight = windowHeight,
|
||||
gallerySection = gallerySection,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
|
||||
package com.microsoft.device.display.samples.navigationrail.ui.view
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
|
@ -22,6 +25,7 @@ import androidx.compose.material.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
@ -41,9 +45,11 @@ import com.microsoft.device.display.samples.navigationrail.ui.components.InfoBox
|
|||
private lateinit var BodyTextStyle: TextStyle
|
||||
private lateinit var SubtitleTextStyle: TextStyle
|
||||
private const val EXPANDED_HEIGHT_2PANE = 0.7f
|
||||
private const val EXPANDED_HEIGHT_1PANE = 0.55f
|
||||
private const val EXPANDED_HEIGHT_1PANE_PORTRAIT = 0.55f
|
||||
private const val EXPANDED_HEIGHT_1PANE_LANDSCAPE = 0.65f
|
||||
private const val COLLAPSED_HEIGHT_2PANE = 0.4f
|
||||
private const val COLLAPSED_HEIGHT_1PANE = 0.28f
|
||||
private const val COLLAPSED_HEIGHT_1PANE_PORTRAIT = 0.28f
|
||||
private const val COLLAPSED_HEIGHT_1PANE_LANDSCAPE = 0.357f
|
||||
private val PILL_TOP_PADDING = 8.dp
|
||||
private val NAME_TOP_PADDING = 8.dp
|
||||
private val LOCATION_TOP_PADDING = 10.dp
|
||||
|
@ -57,18 +63,42 @@ private const val LONG_DETAILS_LINE_HEIGHT = 32f
|
|||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun BoxWithConstraintsScope.ItemDetailsDrawer(
|
||||
modifier: Modifier,
|
||||
image: Image,
|
||||
isDualLandscape: Boolean,
|
||||
foldSize: Dp,
|
||||
isDualPortrait: Boolean,
|
||||
foldOccludes: Boolean,
|
||||
foldBounds: Rect,
|
||||
windowHeight: Dp,
|
||||
gallerySection: GallerySections?,
|
||||
) {
|
||||
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
|
||||
// Set max/min height for drawer based on orientation
|
||||
val expandedHeightPct = if (isDualLandscape) EXPANDED_HEIGHT_2PANE else EXPANDED_HEIGHT_1PANE
|
||||
val collapsedHeightPct = if (isDualLandscape) COLLAPSED_HEIGHT_2PANE else COLLAPSED_HEIGHT_1PANE
|
||||
val fullHeight = constraints.maxHeight.toFloat()
|
||||
val expandedHeight = with(LocalDensity.current) { (expandedHeightPct * fullHeight).toDp() }
|
||||
val collapsedHeight = with(LocalDensity.current) { (collapsedHeightPct * fullHeight).toDp() }
|
||||
val expandedHeightPct: Float
|
||||
val collapsedHeightPct: Float
|
||||
when {
|
||||
isDualLandscape -> {
|
||||
expandedHeightPct = EXPANDED_HEIGHT_2PANE
|
||||
collapsedHeightPct = COLLAPSED_HEIGHT_2PANE
|
||||
}
|
||||
isDualPortrait || isPortrait -> {
|
||||
expandedHeightPct = EXPANDED_HEIGHT_1PANE_PORTRAIT
|
||||
collapsedHeightPct = COLLAPSED_HEIGHT_1PANE_PORTRAIT
|
||||
}
|
||||
else -> {
|
||||
expandedHeightPct = EXPANDED_HEIGHT_1PANE_LANDSCAPE
|
||||
collapsedHeightPct = COLLAPSED_HEIGHT_1PANE_LANDSCAPE
|
||||
}
|
||||
}
|
||||
val foldBoundsDp: RectF
|
||||
with(LocalDensity.current) {
|
||||
val leftDp = foldBounds.left.toDp().value
|
||||
val topDp = foldBounds.top.toDp().value
|
||||
val rightDp = foldBounds.right.toDp().value
|
||||
val bottomDp = foldBounds.bottom.toDp().value
|
||||
|
||||
foldBoundsDp = RectF(leftDp, topDp, rightDp, bottomDp)
|
||||
}
|
||||
|
||||
// Set text size for drawer based on orientation
|
||||
if (isDualLandscape) {
|
||||
|
@ -80,11 +110,12 @@ fun BoxWithConstraintsScope.ItemDetailsDrawer(
|
|||
}
|
||||
|
||||
ContentDrawer(
|
||||
modifier = modifier,
|
||||
expandHeight = expandedHeight,
|
||||
collapseHeight = collapsedHeight,
|
||||
hingeOccludes = isDualLandscape,
|
||||
foldSize = foldSize,
|
||||
expandedHeightPct = expandedHeightPct,
|
||||
collapsedHeightPct = collapsedHeightPct,
|
||||
foldOccludes = foldOccludes && isDualLandscape,
|
||||
foldBoundsDp = foldBoundsDp,
|
||||
foldBottomPaddingDp = LONG_DETAILS_TOP_PADDING,
|
||||
windowHeightDp = windowHeight,
|
||||
hiddenContent = { ItemDetailsLong(image.details) }
|
||||
) {
|
||||
DrawerPill()
|
||||
|
@ -162,7 +193,6 @@ private fun ItemConditions(gallerySection: GallerySections?, fact1: String, fact
|
|||
private fun ItemDetailsLong(details: String) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Spacer(Modifier.height(LONG_DETAILS_TOP_PADDING))
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(bottom = LONG_DETAILS_BOTTOM_PADDING)
|
||||
|
|
10
README.md
10
README.md
|
@ -8,7 +8,7 @@ products:
|
|||
description: "Samples showing how to use Jetpack Compose to achieve dual-screen user interface patterns."
|
||||
urlFragment: all
|
||||
---
|
||||
![build-test-check](https://github.com/microsoft/surface-duo-compose-samples/actions/workflows/build_test_check.yml/badge.svg) ![Compose Version](https://img.shields.io/badge/Jetpack%20Compose-1.0.5-brightgreen)
|
||||
![build-test-check](https://github.com/microsoft/surface-duo-compose-samples/actions/workflows/build_test_check.yml/badge.svg) ![Compose Version](https://img.shields.io/badge/Jetpack%20Compose-1.1.0‐rc03-brightgreen)
|
||||
|
||||
# Surface Duo Jetpack Compose Samples
|
||||
|
||||
|
@ -26,9 +26,11 @@ Please check out our page on [Jetpack Compose for Microsoft Surface Duo](https:/
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- Jetpack Compose version: `1.0.5`
|
||||
- Jetpack Compose version: `1.1.0-rc03`
|
||||
|
||||
- Jetpack WindowManager version: `1.0.0-rc01`
|
||||
- Jetpack WindowManager version: `1.0.0`
|
||||
|
||||
- Android Studio version: Bumblebee `2021.1.1`
|
||||
|
||||
## Microsoft Compose Libraries
|
||||
|
||||
|
@ -55,6 +57,8 @@ The samples are built with Microsoft Compose libraries, [TwoPaneLayout](https://
|
|||
|
||||
## Social links
|
||||
|
||||
- [video: Jetpack Compose WindowState preview](https://www.twitch.tv/videos/1271211220)
|
||||
- [blog: Jetpack Compose WindowState preview](https://devblogs.microsoft.com/surface-duo/jetpack-compose-windowstate-preview/)
|
||||
- [video: Get started with Jetpack Compose Twitch](https://www.youtube.com/watch?v=ijXDWDtdiIE)
|
||||
- [blog: Get started with Jetpack Compose](https://devblogs.microsoft.com/surface-duo/get-started-with-jetpack-compose/)
|
||||
- [video: NavigationRail Compose sample Twitch](https://www.youtube.com/watch?v=pdoIyOU7Suk)
|
||||
|
|
|
@ -31,9 +31,12 @@ android {
|
|||
kotlinCompilerExtensionVersion composeVersion
|
||||
}
|
||||
packagingOptions {
|
||||
exclude "META-INF/licenses/**"
|
||||
exclude "META-INF/AL2.0"
|
||||
exclude "META-INF/LGPL2.1"
|
||||
jniLibs {
|
||||
excludes += ['META-INF/licenses/**']
|
||||
}
|
||||
resources {
|
||||
excludes += ['META-INF/licenses/**', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
*/
|
||||
|
||||
ext {
|
||||
gradlePluginVersion = '7.0.4'
|
||||
kotlinVersion = "1.5.31"
|
||||
gradlePluginVersion = '7.1.0'
|
||||
kotlinVersion = "1.6.10"
|
||||
compileSdkVersion = 31
|
||||
targetSdkVersion = compileSdkVersion
|
||||
minSdkVersion = 23
|
||||
|
@ -21,20 +21,18 @@ ext {
|
|||
]
|
||||
|
||||
// AndroidX versions
|
||||
appCompatVersion = "1.3.0"
|
||||
ktxCoreVersion = "1.5.0"
|
||||
windowVersion = "1.0.0-rc01"
|
||||
appCompatVersion = '1.4.1'
|
||||
ktxCoreVersion = '1.7.0'
|
||||
windowVersion = '1.0.0'
|
||||
androidxDependencies = [
|
||||
appCompat : "androidx.appcompat:appcompat:$appCompatVersion",
|
||||
ktxCore : "androidx.core:core-ktx:$ktxCoreVersion",
|
||||
window : "androidx.window:window:$windowVersion",
|
||||
appCompat : "androidx.appcompat:appcompat:$appCompatVersion",
|
||||
ktxCore : "androidx.core:core-ktx:$ktxCoreVersion",
|
||||
]
|
||||
|
||||
// Compose dependencies
|
||||
composeVersion = "1.0.5"
|
||||
composeUnstableVersion = "1.1.0-rc01"
|
||||
composeVersion = "1.1.0-rc03"
|
||||
activityComposeVersion = "1.4.0"
|
||||
navigationComposeVersion = "2.4.0-rc01"
|
||||
navigationComposeVersion = '2.4.0'
|
||||
composeDependencies = [
|
||||
composeAnimation : "androidx.compose.animation:animation:$composeVersion",
|
||||
composeRuntime : "androidx.compose.runtime:runtime-livedata:$composeVersion",
|
||||
|
@ -43,7 +41,6 @@ ext {
|
|||
composeUITooling : "androidx.compose.ui:ui-tooling:$composeVersion",
|
||||
activityCompose : "androidx.activity:activity-compose:$activityComposeVersion",
|
||||
navigationCompose : "androidx.navigation:navigation-compose:$navigationComposeVersion",
|
||||
composeMaterialForNavRail: "androidx.compose.material:material:$composeUnstableVersion",
|
||||
]
|
||||
|
||||
// Testing versions
|
||||
|
@ -67,7 +64,7 @@ ext {
|
|||
]
|
||||
|
||||
// Google dependencies
|
||||
materialVersion = "1.5.0-alpha01"
|
||||
materialVersion = '1.5.0'
|
||||
googleDependencies = [
|
||||
material: "com.google.android.material:material:$materialVersion"
|
||||
]
|
||||
|
@ -76,7 +73,7 @@ ext {
|
|||
twoPaneLayoutVersion = "1.0.0-alpha10"
|
||||
windowStateVersion = "1.0.0-alpha1"
|
||||
microsoftDependencies = [
|
||||
twoPaneLayout: "com.microsoft.device.dualscreen:twopanelayout:$twoPaneLayoutVersion",
|
||||
windowState: "com.microsoft.device.dualscreen:windowstate:$windowStateVersion",
|
||||
twoPaneLayout : "com.microsoft.device.dualscreen:twopanelayout:$twoPaneLayoutVersion",
|
||||
windowState : "com.microsoft.device.dualscreen:windowstate:$windowStateVersion",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
#Thu Feb 25 11:36:45 PST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -18,7 +18,7 @@ dependencies {
|
|||
task ktlint(type: JavaExec, group: "verification") {
|
||||
description = "Check Kotlin code style."
|
||||
classpath = configurations.ktlint
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
mainClass.set("com.pinterest.ktlint.Main")
|
||||
args "src/**/*.kt"
|
||||
// to generate report in checkstyle format prepend following args:
|
||||
// "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml"
|
||||
|
@ -28,6 +28,6 @@ task ktlint(type: JavaExec, group: "verification") {
|
|||
task ktlintFormat(type: JavaExec, group: "formatting") {
|
||||
description = "Fix Kotlin code style deviations."
|
||||
classpath = configurations.ktlint
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
mainClass.set("com.pinterest.ktlint.Main")
|
||||
args "-F", "src/**/*.kt"
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче