[go: nahoru, domu]

blob: a03a041b846e0882075a40f850ee19b765bb50a9 [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.surface
import androidx.compose.Composable
import androidx.ui.core.Clip
import androidx.ui.core.CurrentTextStyleProvider
import androidx.ui.core.Draw
import androidx.ui.core.DrawShadow
import androidx.ui.core.Layout
import androidx.ui.core.Modifier
import androidx.ui.core.Text
import androidx.ui.foundation.Border
import androidx.ui.foundation.DrawBackground
import androidx.ui.foundation.DrawBorder
import androidx.ui.foundation.ProvideContentColor
import androidx.ui.foundation.shape.RectangleShape
import androidx.ui.graphics.Color
import androidx.ui.graphics.Shape
import androidx.ui.graphics.compositeOver
import androidx.ui.material.ColorPalette
import androidx.ui.material.MaterialTheme
import androidx.ui.material.contentColorFor
import androidx.ui.text.TextStyle
import androidx.ui.unit.Dp
import androidx.ui.unit.dp
import androidx.ui.unit.ipx
import kotlin.math.ln
/**
* The [Surface] is responsible for:
*
* 1) Clipping: Surface clips its children to the shape specified by [shape]
*
* 2) Elevation: Surface elevates its children on the Z axis by [elevation] pixels,
* and draws the appropriate shadow.
*
* 3) Borders: If [shape] has a border, then it will also be drawn.
*
* Material surface is the central metaphor in material design. Each surface
* exists at a given elevation, which influences how that piece of surface
* visually relates to other surfaces and how that surface casts shadows.
*
* [contentColor] is the preferred color for any children inside this surface - any [Text] inside
* this Surface will use this color by default.
*
* If no [contentColor] is set, this surface will try and match its background color to a color
* defined in the theme [ColorPalette], and return the corresponding `onFoo` color. For example,
* if the [color] of this surface is [ColorPalette.surface], [contentColor] will be set to
* [ColorPalette.onSurface]. If [color] is not part of the theme palette, [contentColor] will keep
* the same value set above this Surface.
*
* To modify these default style values used by text, use [CurrentTextStyleProvider] or explicitly
* pass a new [TextStyle] to your text.
*
* To manually retrieve the content color inside a surface, use [contentColor].
*
* @param modifier Modifier to be applied to the layout corresponding to the surface
* @param shape Defines the surface's shape as well its shadow. A shadow is only
* displayed if the [elevation] is greater than zero.
* @param color The background color. Use [Color.Transparent] to have no color.
* @param contentColor The preferred content color provided by this Surface to its children.
* Defaults to either the matching `onFoo` color for [color], or if [color] is not a color from
* the theme, this will keep the same value set above this Surface.
* @param border Optional border to draw on top of the surface
* @param elevation The z-coordinate at which to place this surface. This controls
* the size of the shadow below the surface.
*/
@Composable
fun Surface(
modifier: Modifier = Modifier.None,
shape: Shape = RectangleShape,
color: Color = MaterialTheme.colors().surface,
contentColor: Color = contentColorFor(color),
border: Border? = null,
elevation: Dp = 0.dp,
children: @Composable() () -> Unit
) {
val borderModifier =
if (border != null) DrawBorder(border, shape) else Modifier.None
SurfaceLayout(modifier + borderModifier) {
if (elevation > 0.dp) {
DrawShadow(shape = shape, elevation = elevation)
}
val backgroundColor = getBackgroundColorForElevation(color, elevation)
val background = DrawBackground(shape = shape, color = backgroundColor)
@Suppress("DEPRECATION") // remove when b/147606015 is fixed
Draw { canvas, size ->
background.draw(this, {}, canvas, size)
}
Clip(shape = shape) {
ProvideContentColor(contentColor, children)
}
}
}
/**
* primarySurface represents the background color of components that are [ColorPalette.primary]
* in light theme, and [ColorPalette.surface] in dark theme, such as [androidx.ui.material.TabRow]
* and [androidx.ui.material.TopAppBar]. This is to reduce brightness of large surfaces in dark
* theme, aiding contrast and readability. See
* [Dark Theme](https://material.io/design/color/dark-theme.html#custom-application).
*
* @return [ColorPalette.primary] if in light theme, else [ColorPalette.surface]
*/
val ColorPalette.primarySurface: Color get() = if (isLight) primary else surface
/**
* A simple layout which just reserves a space for a [Surface].
* It positions the only child in the left top corner.
*/
// TODO("Andrey: Should be replaced with some basic layout implementation when we have it")
@Composable
private fun SurfaceLayout(modifier: Modifier = Modifier.None, children: @Composable() () -> Unit) {
Layout(children, modifier) { measurables, constraints, _ ->
if (measurables.size > 1) {
throw IllegalStateException("Surface can have only one direct measurable child!")
}
val measurable = measurables.firstOrNull()
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)
}
}
}
}
/**
* If in a light theme, returns [color]. If in dark theme, applies an elevation overlay if [color]
* is equal to ColorPalette.surface, else returns [color]
*/
@Composable
private fun getBackgroundColorForElevation(color: Color, elevation: Dp): Color {
val colors = MaterialTheme.colors()
return if (elevation > 0.dp && color == colors.surface && !colors.isLight) {
color.withElevation(elevation)
} else {
color
}
}
/**
* Applies a [Color.White] overlay to this color based on the [elevation]. This increases visibility
* of elevation for surfaces in a dark theme.
*/
private fun Color.withElevation(elevation: Dp): Color {
val foreground = calculateForeground(elevation)
return foreground.compositeOver(this)
}
// TODO: b/145802792 - clarify this algorithm
/**
* @return the alpha-modified [Color.White] to overlay on top of the surface color to produce
* the resultant color.
*/
private fun calculateForeground(elevation: Dp): Color {
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
return Color.White.copy(alpha = alpha)
}