[go: nahoru, domu]

blob: c42952eb8b0fe7c7e6a359693739a68f47a9e217 [file] [log] [blame]
/*
* Copyright 2018 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.FloatPropKey
import androidx.animation.TransitionSpec
import androidx.animation.transitionDefinition
import androidx.compose.Composable
import androidx.compose.remember
import androidx.ui.animation.ColorPropKey
import androidx.ui.animation.Transition
import androidx.ui.core.DensityAmbient
import androidx.ui.core.Modifier
import androidx.ui.foundation.Box
import androidx.ui.foundation.Canvas
import androidx.ui.foundation.ContentGravity
import androidx.ui.foundation.selection.ToggleableState
import androidx.ui.foundation.selection.TriStateToggleable
import androidx.ui.geometry.Offset
import androidx.ui.geometry.RRect
import androidx.ui.geometry.Radius
import androidx.ui.geometry.Size
import androidx.ui.geometry.outerRect
import androidx.ui.geometry.shrink
import androidx.ui.graphics.ClipOp
import androidx.ui.graphics.Color
import androidx.ui.graphics.StrokeCap
import androidx.ui.graphics.painter.CanvasScope
import androidx.ui.graphics.painter.Stroke
import androidx.ui.graphics.painter.clipRect
import androidx.ui.layout.padding
import androidx.ui.layout.preferredSize
import androidx.ui.material.ripple.ripple
import androidx.ui.semantics.Semantics
import androidx.ui.unit.dp
/**
* A component that represents two states (checked / unchecked).
*
* @sample androidx.ui.material.samples.CheckboxSample
*
* @see [TriStateCheckbox] if you require support for an indeterminate state.
*
* @param checked whether Checkbox is checked or unchecked
* @param onCheckedChange callback to be invoked when checkbox is being clicked,
* therefore the change of checked state in requested.
* @param enabled enabled whether or not this [Checkbox] will handle input events and appear
* enabled for semantics purposes
* @param modifier Modifier to be applied to the layout of the checkbox
* @param color custom color for checkbox
*/
@Composable
fun Checkbox(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
enabled: Boolean = true,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.secondary
) {
TriStateCheckbox(
state = ToggleableState(checked),
onClick = { onCheckedChange(!checked) },
enabled = enabled,
color = color,
modifier = modifier
)
}
/**
* A TriStateCheckbox is a toggleable component that provides
* checked / unchecked / indeterminate options.
* <p>
* A TriStateCheckbox should be used when there are
* dependent checkboxes associated to this component and those can have different values.
*
* @sample androidx.ui.material.samples.TriStateCheckboxSample
*
* @see [Checkbox] if you want a simple component that represents Boolean state
*
* @param state whether TriStateCheckbox is checked, unchecked or in indeterminate state
* @param onClick callback to be invoked when checkbox is being clicked,
* therefore the change of ToggleableState state is requested.
* @param enabled enabled whether or not this [TriStateCheckbox] will handle input events and
* appear enabled for semantics purposes
* @param modifier Modifier to be applied to the layout of the checkbox
* @param color custom color for checkbox
*/
@Composable
fun TriStateCheckbox(
state: ToggleableState,
onClick: () -> Unit,
enabled: Boolean = true,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.secondary
) {
Semantics(container = true, mergeAllDescendants = true) {
Box(modifier, gravity = ContentGravity.Center) {
TriStateToggleable(
state = state,
onClick = onClick,
enabled = enabled,
modifier = Modifier.ripple(bounded = false, enabled = enabled)
) {
DrawCheckbox(
value = state,
activeColor = color,
modifier = CheckboxDefaultPadding
)
}
}
}
}
@Composable
private fun DrawCheckbox(value: ToggleableState, activeColor: Color, modifier: Modifier) {
val unselectedColor = MaterialTheme.colors.onSurface.copy(alpha = UncheckedBoxOpacity)
val definition = remember(activeColor, unselectedColor) {
generateTransitionDefinition(activeColor, unselectedColor)
}
Transition(definition = definition, toState = value) { state ->
val strokeWidthPx: Float
val radiusPx: Float
with(DensityAmbient.current) {
strokeWidthPx = StrokeWidth.toPx().value
radiusPx = RadiusSize.toPx().value
}
Canvas(modifier.preferredSize(CheckboxSize)) {
drawBox(
color = state[BoxColorProp],
innerRadiusFraction = state[InnerRadiusFractionProp],
radius = radiusPx,
strokeWidth = strokeWidthPx
)
drawCheck(
checkFraction = state[CheckFractionProp],
crossCenterGravitation = state[CenterGravitationForCheck],
strokeWidthPx = strokeWidthPx
)
}
}
}
private fun CanvasScope.drawBox(
color: Color,
innerRadiusFraction: Float,
radius: Float,
strokeWidth: Float
) {
val halfStrokeWidth = strokeWidth / 2.0f
val stroke = Stroke(strokeWidth)
val checkboxSize = size.width
val outer = RRect(
halfStrokeWidth,
halfStrokeWidth,
checkboxSize - halfStrokeWidth,
checkboxSize - halfStrokeWidth,
Radius.circular(radius)
)
// Determine whether or not we need to offset the inset by a pixel
// to ensure that there is no gap between the outer stroked round rect
// and the inner rect.
val offset = (halfStrokeWidth - halfStrokeWidth.toInt()) + 0.5f
// TODO(malkov): this radius formula is not in material spec
// If the inner region is to be filled such that it is larger than the outer stroke size
// then create a difference clip to draw the stroke outside of the rectangular region
// to be drawn within the interior rectangle. This is done to ensure that pixels do
// not overlap which might cause unexpected blending if the target color has some
// opacity. If the inner region is not to be drawn or will occupy a smaller width than
// the outer stroke then just draw the outer stroke
val innerStrokeWidth = innerRadiusFraction * checkboxSize / 2
if (innerStrokeWidth > strokeWidth) {
val clipRect = outer.shrink(strokeWidth / 2 - offset).outerRect()
clipRect(clipRect.left, clipRect.top, clipRect.right, clipRect.bottom, ClipOp.difference) {
drawRoundRect(
color,
Offset(outer.left, outer.top),
Size(outer.width, outer.height),
radius,
style = stroke
)
}
clipRect(clipRect.left, clipRect.top, clipRect.right, clipRect.bottom) {
val innerHalfStrokeWidth = innerStrokeWidth / 2
val rect = outer.shrink(innerHalfStrokeWidth - offset).outerRect()
drawRect(
color = color,
topLeft = Offset(rect.left, rect.top),
size = Size(rect.width, rect.height),
style = Stroke(innerStrokeWidth)
)
}
} else {
drawRoundRect(
color,
topLeft = Offset(outer.left, outer.top),
size = Size(outer.width, outer.height),
radiusX = radius,
radiusY = radius,
style = stroke
)
}
}
private fun CanvasScope.drawCheck(
checkFraction: Float,
crossCenterGravitation: Float,
strokeWidthPx: Float
) {
val stroke = Stroke(width = strokeWidthPx, cap = StrokeCap.square)
val width = size.width
val checkCrossX = 0.4f
val checkCrossY = 0.7f
val leftX = 0.2f
val leftY = 0.5f
val rightX = 0.8f
val rightY = 0.3f
val gravitatedCrossX = calcMiddleValue(checkCrossX, 0.5f, crossCenterGravitation)
val gravitatedCrossY = calcMiddleValue(checkCrossY, 0.5f, crossCenterGravitation)
// gravitate only Y for end to achieve center line
val gravitatedLeftY = calcMiddleValue(leftY, 0.5f, crossCenterGravitation)
val gravitatedRightY = calcMiddleValue(rightY, 0.5f, crossCenterGravitation)
val crossPoint = Offset(width * gravitatedCrossX, width * gravitatedCrossY)
val rightBranch = Offset(
width * calcMiddleValue(gravitatedCrossX, rightX, checkFraction),
width * calcMiddleValue(gravitatedCrossY, gravitatedRightY, checkFraction)
)
val leftBranch = Offset(
width * calcMiddleValue(gravitatedCrossX, leftX, checkFraction),
width * calcMiddleValue(gravitatedCrossY, gravitatedLeftY, checkFraction)
)
drawLine(CheckStrokeDefaultColor, crossPoint, leftBranch, stroke)
drawLine(CheckStrokeDefaultColor, crossPoint, rightBranch, stroke)
}
private fun calcMiddleValue(start: Float, finish: Float, fraction: Float): Float {
return start * (1 - fraction) + finish * fraction
}
// all float props are fraction now [0f .. 1f] as it seems convenient
private val InnerRadiusFractionProp = FloatPropKey()
private val CheckFractionProp = FloatPropKey()
private val CenterGravitationForCheck = FloatPropKey()
private val BoxColorProp = ColorPropKey()
private val BoxAnimationDuration = 100
private val CheckStrokeAnimationDuration = 100
private fun generateTransitionDefinition(color: Color, unselectedColor: Color) =
transitionDefinition {
state(ToggleableState.On) {
this[CheckFractionProp] = 1f
this[InnerRadiusFractionProp] = 1f
this[CenterGravitationForCheck] = 0f
this[BoxColorProp] = color
}
state(ToggleableState.Off) {
this[CheckFractionProp] = 0f
this[InnerRadiusFractionProp] = 0f
this[CenterGravitationForCheck] = 1f
this[BoxColorProp] = unselectedColor
}
state(ToggleableState.Indeterminate) {
this[CheckFractionProp] = 1f
this[InnerRadiusFractionProp] = 1f
this[CenterGravitationForCheck] = 1f
this[BoxColorProp] = color
}
transition(fromState = ToggleableState.Off, toState = ToggleableState.On) {
boxTransitionFromUnchecked()
CenterGravitationForCheck using snap()
}
transition(fromState = ToggleableState.On, toState = ToggleableState.Off) {
boxTransitionToUnchecked()
CenterGravitationForCheck using tween {
duration = CheckStrokeAnimationDuration
}
}
transition(
ToggleableState.On to ToggleableState.Indeterminate,
ToggleableState.Indeterminate to ToggleableState.On
) {
CenterGravitationForCheck using tween {
duration = CheckStrokeAnimationDuration
}
}
transition(fromState = ToggleableState.Indeterminate, toState = ToggleableState.Off) {
boxTransitionToUnchecked()
}
transition(fromState = ToggleableState.Off, toState = ToggleableState.Indeterminate) {
boxTransitionFromUnchecked()
}
}
private fun TransitionSpec<ToggleableState>.boxTransitionFromUnchecked() {
BoxColorProp using snap()
InnerRadiusFractionProp using tween {
duration = BoxAnimationDuration
}
CheckFractionProp using tween {
duration = CheckStrokeAnimationDuration
delay = BoxAnimationDuration
}
}
private fun TransitionSpec<ToggleableState>.boxTransitionToUnchecked() {
BoxColorProp using snap()
InnerRadiusFractionProp using tween {
duration = BoxAnimationDuration
delay = CheckStrokeAnimationDuration
}
CheckFractionProp using tween {
duration = CheckStrokeAnimationDuration
}
}
private val CheckboxDefaultPadding = Modifier.padding(2.dp)
private val CheckboxSize = 20.dp
private val StrokeWidth = 2.dp
private val RadiusSize = 2.dp
private val UncheckedBoxOpacity = 0.6f
private val CheckStrokeDefaultColor = Color.White