[go: nahoru, domu]

blob: 155d9b85ccac3d66a3a6b2d29c00fe5457a4e1de [file] [log] [blame]
/*
* Copyright 2019 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
*
* http://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 androidx.ui.material
import androidx.animation.PhysicsBuilder
import androidx.compose.Composable
import androidx.compose.ambient
import androidx.compose.memo
import androidx.compose.state
import androidx.compose.unaryPlus
import androidx.ui.core.Alignment
import androidx.ui.core.Constraints
import androidx.ui.core.ContextAmbient
import androidx.ui.core.Draw
import androidx.ui.core.IntPx
import androidx.ui.core.Layout
import androidx.ui.core.Px
import androidx.ui.core.RepaintBoundary
import androidx.ui.core.ambientDensity
import androidx.ui.core.dp
import androidx.ui.core.hasBoundedHeight
import androidx.ui.core.hasBoundedWidth
import androidx.ui.core.ipx
import androidx.ui.core.min
import androidx.ui.core.px
import androidx.ui.core.toRect
import androidx.ui.core.withDensity
import androidx.ui.foundation.Clickable
import androidx.ui.foundation.ValueHolder
import androidx.ui.foundation.gestures.DragDirection
import androidx.ui.graphics.Paint
import androidx.ui.graphics.PaintingStyle
import androidx.ui.layout.Container
import androidx.ui.layout.DpConstraints
import androidx.ui.layout.EdgeInsets
import androidx.ui.layout.Stack
import androidx.ui.lerp
import androidx.ui.material.internal.StateDraggable
import androidx.ui.material.surface.Surface
/**
* Possible states of the drawer
*/
enum class DrawerState {
/**
* Constant to indicate the state of the drawer when it's closed
*/
Closed,
/**
* Constant to indicate the state of the drawer when it's opened
*/
Opened,
// Expanded
}
/**
* Navigation drawers provide access to destinations in your app.
*
* Standard navigation drawers allow interaction with both screen content and the drawer
* at the same time. They can be used on tablet and desktop,
* but they are not suitable for mobile due to limited screen size.
*
* See [ModalDrawerLayout] and [BottomDrawerLayout] for more mobile friendly options.
*
* @sample androidx.ui.material.samples.StaticDrawerSample
*
* @param drawerContent composable that represents content inside the drawer
*/
@Composable
fun StaticDrawer(
drawerContent: @Composable() () -> Unit
) {
Container(width = StaticDrawerWidth, expanded = true, children = drawerContent)
}
/**
* Navigation drawers provide access to destinations in your app.
*
* Modal navigation drawers block interaction with the rest of an app’s content with a scrim.
* They are elevated above most of the app’s UI and don’t affect the screen’s layout grid.
*
* See [StaticDrawer] for always visible drawer, suitable for tablet or desktop.
* See [BottomDrawerLayout] for a layout that introduces a bottom drawer, suitable when
* using bottom navigation.
*
* @sample androidx.ui.material.samples.ModalDrawerSample
*
* @param drawerState state of the drawer
* @param onStateChange lambda to be invoked when the drawer requests to change its state,
* e.g. when the drawer is being swiped to the new state or when the scrim is clicked
* @param gesturesEnabled whether or not drawer can be interacted by gestures
* @param drawerContent composable that represents content inside the drawer
* @param bodyContent content of the rest of the UI
*
* @throws IllegalStateException when parent has [Px.Infinity] width
*/
@Composable
fun ModalDrawerLayout(
drawerState: DrawerState,
onStateChange: (DrawerState) -> Unit,
gesturesEnabled: Boolean = true,
drawerContent: @Composable() () -> Unit,
bodyContent: @Composable() () -> Unit
) {
Container(expanded = true) {
NoCompositionWithConstraints { pxConstraints ->
// TODO : think about Infinite max bounds case
if (!pxConstraints.hasBoundedWidth) {
throw IllegalStateException("Drawer shouldn't have infinite width")
}
val constraints = withDensity(+ambientDensity()) {
DpConstraints(pxConstraints)
}
val minValue = -pxConstraints.maxWidth.value.toFloat()
val maxValue = 0f
val anchors = listOf(minValue to DrawerState.Closed, maxValue to DrawerState.Opened)
StateDraggable(
state = drawerState,
onStateChange = onStateChange,
anchorsToState = anchors,
animationBuilder = AnimationBuilder,
dragDirection = DragDirection.Horizontal,
minValue = minValue,
maxValue = maxValue,
enabled = gesturesEnabled
) { model ->
Stack {
aligned(Alignment.TopLeft) {
bodyContent()
Scrim(drawerState, onStateChange, fraction = {
calculateFraction(minValue, maxValue, model.value)
})
DrawerContent(model, constraints, drawerContent)
}
}
}
}
}
}
/**
* Navigation drawers provide access to destinations in your app.
*
* Bottom navigation drawers are modal drawers that are anchored
* to the bottom of the screen instead of the left or right edge.
* They are only used with bottom app bars.
*
* These drawers open upon tapping the navigation menu icon in the bottom app bar.
* They are only for use on mobile.
*
* See [StaticDrawer] for always visible drawer, suitable for tablet or desktop
* See [ModalDrawerLayout] for a layout that introduces a classic from-the-side drawer.
*
* @sample androidx.ui.material.samples.BottomDrawerSample
*
* @param drawerState state of the drawer
* @param onStateChange lambda to be invoked when the drawer requests to change its state,
* e.g. when the drawer is being swiped to the new state or when the scrim is clicked
* @param gesturesEnabled whether or not drawer can be interacted by gestures
* @param drawerContent composable that represents content inside the drawer
* @param bodyContent content of the rest of the UI
*
* @throws IllegalStateException when parent has [Px.Infinity] height
*/
@Composable
fun BottomDrawerLayout(
drawerState: DrawerState,
onStateChange: (DrawerState) -> Unit,
gesturesEnabled: Boolean = true,
drawerContent: @Composable() () -> Unit,
bodyContent: @Composable() () -> Unit
) {
Container(expanded = true) {
NoCompositionWithConstraints { pxConstraints ->
// TODO : think about Infinite max bounds case
if (!pxConstraints.hasBoundedHeight) {
throw IllegalStateException("Drawer shouldn't have infinite height")
}
val constraints = withDensity(+ambientDensity()) {
DpConstraints(pxConstraints)
}
val minValue = 0f
val maxValue = pxConstraints.maxHeight.value.toFloat()
// TODO: add proper landscape support
val isLandscape = constraints.maxWidth > constraints.maxHeight
val openedValue = if (isLandscape) maxValue else lerp(
minValue,
maxValue,
BottomDrawerOpenFraction
)
val anchors = listOf(
maxValue to DrawerState.Closed,
openedValue to DrawerState.Opened,
minValue to DrawerState.Opened
)
StateDraggable(
state = drawerState,
onStateChange = onStateChange,
anchorsToState = anchors,
animationBuilder = AnimationBuilder,
dragDirection = DragDirection.Vertical,
minValue = minValue,
maxValue = maxValue,
enabled = gesturesEnabled
) { model ->
Stack {
aligned(Alignment.TopLeft) {
bodyContent()
Scrim(drawerState, onStateChange, fraction = {
// as we scroll "from height to 0" , need to reverse fraction
1 - calculateFraction(openedValue, maxValue, model.value)
})
BottomDrawerContent(model, constraints, drawerContent)
}
}
}
}
}
}
@Composable
private fun DrawerContent(
xOffset: ValueHolder<Float>,
constraints: DpConstraints,
children: @Composable() () -> Unit
) {
WithOffset(xOffset = xOffset) {
Container(
constraints = constraints,
padding = EdgeInsets(right = VerticalDrawerPadding)
) {
// remove Container when we will support multiply children
Surface { Container(expanded = true, children = children) }
}
}
}
@Composable
private fun BottomDrawerContent(
yOffset: ValueHolder<Float>,
constraints: DpConstraints,
children: @Composable() () -> Unit
) {
WithOffset(yOffset = yOffset) {
Container(constraints = constraints) {
// remove Container when we will support multiply children
Surface { Container(expanded = true, children = children) }
}
}
}
private fun calculateFraction(a: Float, b: Float, pos: Float) =
((pos - a) / (b - a)).coerceIn(0f, 1f)
@Composable
private fun Scrim(
state: DrawerState,
onStateChange: (DrawerState) -> Unit,
fraction: () -> Float
) {
// TODO: use enabled = false here when it will be available
val scrimContent = @Composable {
Container(expanded = true) {
val paint = +memo { Paint().apply { style = PaintingStyle.fill } }
val color = (+MaterialTheme.colors()).onSurface
Draw { canvas, parentSize ->
val scrimAlpha = fraction() * ScrimDefaultOpacity
paint.color = color.copy(alpha = scrimAlpha)
canvas.drawRect(parentSize.toRect(), paint)
}
}
}
if (state == DrawerState.Opened) {
Clickable(onClick = { onStateChange(DrawerState.Closed) }, children = scrimContent)
} else {
scrimContent()
}
}
// TODO: consider make pretty and move to public
@Composable
private fun WithOffset(
xOffset: ValueHolder<Float>? = null,
yOffset: ValueHolder<Float>? = null,
child: @Composable() () -> Unit
) {
Layout(children = {
RepaintBoundary(children = child)
}) { measurables, constraints ->
if (measurables.size > 1) {
throw IllegalStateException("Only one child is allowed")
}
val childMeasurable = measurables.firstOrNull()
val placeable = childMeasurable?.measure(constraints)
val width: IntPx
val height: IntPx
if (placeable == null) {
width = constraints.minWidth
height = constraints.minHeight
} else {
width = min(placeable.width, constraints.maxWidth)
height = min(placeable.height, constraints.maxHeight)
}
layout(width, height) {
val offX = xOffset?.value?.px ?: 0.px
val offY = yOffset?.value?.px ?: 0.px
placeable?.place(offX, offY)
}
}
}
// TODO (malkov): This adhoc handmade WithConstrains is to ensure we work fast enough in
// DrawerAnimation as well as ensure that all the children will recompose/relayout properly
// Remove when WithConstraints will be fast enough and work properly with relayouting on @
@Composable
private fun NoCompositionWithConstraints(children: @Composable() (Constraints) -> Unit) {
val dm = (+ambient(ContextAmbient)).resources.displayMetrics
// as a default, we emulate full-activity constraints (which is most cases will be exactly what
// we want) and then wait for layout to know proper constraints
var stateConstraints by +state {
Constraints(
minWidth = 0.ipx,
minHeight = 0.ipx,
maxWidth = dm.widthPixels.ipx,
maxHeight = dm.heightPixels.ipx
)
}
Layout({ children(stateConstraints) }) { measurables, constraints ->
if (measurables.size > 1) {
throw IllegalStateException(
"NoCompositionWithConstraints can have only one direct measurable child!"
)
}
val measurable = measurables.firstOrNull()
if (constraints != stateConstraints) stateConstraints = constraints
if (measurable == null) {
layout(constraints.minWidth, constraints.minHeight) {}
} else {
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0.ipx, 0.ipx)
}
}
}
}
private val ScrimDefaultOpacity = 0.32f
private val VerticalDrawerPadding = 56.dp
// drawer children specs
private val StaticDrawerWidth = 256.dp
private val DrawerStiffness = 1000f
private val AnimationBuilder =
PhysicsBuilder<Float>().apply {
stiffness = DrawerStiffness
}
private val BottomDrawerOpenFraction = 0.5f