[go: nahoru, domu]

blob: 97f3f8fda05fefbc5f4e6c399ba0dfb88ba94980 [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.material
import androidx.annotation.CallSuper
import androidx.ui.VoidCallback
import androidx.ui.animation.Curve
import androidx.ui.animation.Curves
import androidx.ui.animation.Tween
import androidx.ui.assert
import androidx.ui.core.Duration
import androidx.ui.engine.geometry.Offset
import androidx.ui.engine.geometry.Rect
import androidx.ui.engine.geometry.Size
import androidx.ui.engine.text.TextDirection
import androidx.ui.engine.text.TextStyle
import androidx.ui.foundation.Key
import androidx.ui.foundation.diagnostics.DiagnosticPropertiesBuilder
import androidx.ui.foundation.diagnostics.DiagnosticsProperty
import androidx.ui.foundation.diagnostics.FloatProperty
import androidx.ui.foundation.diagnostics.EnumProperty
import androidx.ui.painting.Canvas
import androidx.ui.painting.Color
import androidx.ui.painting.borderradius.BorderRadius
import androidx.ui.painting.borders.CircleBorder
import androidx.ui.painting.borders.RoundedRectangleBorder
import androidx.ui.painting.borders.ShapeBorder
import androidx.ui.rendering.box.RenderBox
import androidx.ui.rendering.custompaint.CustomPainter
import androidx.ui.rendering.obj.PaintingContext
import androidx.ui.rendering.obj.RenderObject
import androidx.ui.rendering.proxybox.RenderProxyBox
import androidx.ui.rendering.proxybox.ShapeBorderClipper
import androidx.ui.scheduler.ticker.TickerProvider
import androidx.ui.vectormath64.Matrix4
import androidx.ui.widgets.basic.ClipPath
import androidx.ui.widgets.basic.CustomPaint
import androidx.ui.widgets.basic.Directionality
import androidx.ui.widgets.basic.PhysicalShape
import androidx.ui.widgets.framework.BuildContext
import androidx.ui.widgets.framework.SingleChildRenderObjectWidget
import androidx.ui.widgets.framework.State
import androidx.ui.widgets.framework.StatefulWidget
import androidx.ui.widgets.framework.StatelessWidget
import androidx.ui.widgets.framework.TypeMatcher
import androidx.ui.widgets.framework.Widget
import androidx.ui.widgets.framework.key.GlobalKey
import androidx.ui.widgets.implicitanimation.AnimatedWidgetBaseState
import androidx.ui.widgets.implicitanimation.ImplicitlyAnimatedWidget
import androidx.ui.widgets.implicitanimation.TweenVisitor
import androidx.ui.widgets.tickerprovider.TickerProviderStateMixin
/**
* Signature for the callback used by ink effects to obtain the RECTANGLE for the effect.
*
* Used by [InkHighlight] and [InkSplash], for example.
*/
typealias RectCallback = () -> Rect
/**
* The various kinds of material in material design. Used to
* configure the default behavior of [Material] widgets.
*
* See also:
*
* * [Material], in particular [Material.type]
* * [MaterialEdges]
*/
enum class MaterialType {
/** Rectangle using default theme canvas color. */
CANVAS,
/** Rounded edges, card theme color. */
CARD,
/** A circle, no color by default (used for floating action buttons). */
CIRCLE,
/** Rounded edges, no color by default (used for [MaterialButton] buttons). */
BUTTON,
/**
* A transparent piece of material that draws ink splashes and highlights.
*
* While the material metaphor describes child widgets as printed on the
* material itself and do not hide ink effects, in practice the [Material]
* widget draws child widgets on top of the ink effects.
* A [Material] with type transparency can be placed on top of opaque widgets
* to show ink effects on top of them.
*
* Prefer using the [Ink] widget for showing ink effects on top of opaque
* widgets.
*/
TRANSPARENCY
}
/**
* The border radii used by the various kinds of material in material design.
*
* See also:
*
* * [MaterialType]
* * [Material]
*/
val MaterialEdges = mapOf(
MaterialType.CANVAS to null,
MaterialType.CARD to BorderRadius.circular(2.0f),
MaterialType.CIRCLE to null,
MaterialType.BUTTON to BorderRadius.circular(2.0f),
MaterialType.TRANSPARENCY to null
)
/** The default radius of an ink splash in logical pixels. */
// TODO("Migration|Andrey: Defined in dps, but will actually be drawn as pixels! Solve it!")
const val DefaultSplashRadius: Float = 35.0f
/**
* An interface for creating [InkSplash]s and [InkHighlight]s on a material.
*
* Typically obtained via [Material.of].
*/
interface MaterialInkController {
/** The color of the material. */
val color: Color?
/**
* The ticker provider used by the controller.
*
* Ink features that are added to this controller with [addInkFeature] should
* use this vsync to drive their animations.
*/
val vsync: TickerProvider
/**
* Add an [InkFeature], such as an [InkSplash] or an [InkHighlight].
*
* The ink feature will paint as part of this controller.
*/
fun addInkFeature(feature: InkFeature)
/** Notifies the controller that one of its ink features needs to repaint. */
fun markNeedsPaint()
}
/**
* A piece of material.
*
* The Material widget is responsible for:
*
* 1. Clipping: Material clips its widget sub-tree to the shape specified by
* [shape], [type], and [borderRadius].
* 2. Elevation: Material elevates its widget sub-tree on the Z axis by
* [elevation] pixels, and draws the appropriate shadow.
* 3. Ink effects: Material shows ink effects implemented by [InkFeature]s
* like [InkSplash] and [InkHighlight] below its children.
*
* ## The Material Metaphor
*
* Material is the central metaphor in material design. Each piece of material
* exists at a given elevation, which influences how that piece of material
* visually relates to other pieces of material and how that material casts
* shadows.
*
* Most user interface elements are either conceptually printed on a piece of
* material or themselves made of material. Material reacts to user input using
* [InkSplash] and [InkHighlight] effects. To trigger a reaction on the
* material, use a [MaterialInkController] obtained via [Material.of].
*
* In general, the features of a [Material] should not change over time (e.g. a
* [Material] should not change its [color], [shadowColor] or [type]).
* Changes to [elevation] and [shadowColor] are animated for [animationDuration].
* Changes to [shape] are animated if [type] is not [MaterialType.TRANSPARENCY]
* and [ShapeBorder.lerp] between the previous and next [shape] values is
* supported. Shape changes are also animated for [animationDuration].
*
*
* ## Shape
*
* The shape for material is determined by [shape], [type], and [borderRadius].
*
* - If [shape] is non null, it determines the shape.
* - If [shape] is null and [borderRadius] is non null, the shape is a
* rounded rectangle, with corners specified by [borderRadius].
* - If [shape] and [borderRadius] are null, [type] determines the
* shape as follows:
* - [MaterialType.CANVAS]: the default material shape is a rectangle.
* - [MaterialType.CARD]: the default material shape is a rectangle with
* rounded edges. The edge radii is specified by [MaterialEdges].
* - [MaterialType.CIRCLE]: the default material shape is a CIRCLE.
* - [MaterialType.BUTTON]: the default material shape is a rectangle with
* rounded edges. The edge radii is specified by [MaterialEdges].
* - [MaterialType.TRANSPARENCY]: the default material shape is a rectangle.
*
* ## Border
*
* If [shape] is not null, then its border will also be painted (if any).
*
* ## Layout change notifications
*
* If the layout changes (e.g. because there"s a list on the material, and it"s
* been scrolled), a [LayoutChangedNotification] must be dispatched at the
* relevant subtree. This in particular means that transitions (e.g.
* [SlideTransition]) should not be placed inside [Material] widgets so as to
* move subtrees that contain [InkResponse]s, [InkWell]s, [Ink]s, or other
* widgets that use the [InkFeature] mechanism. Otherwise, in-progress ink
* features (e.g., ink splashes and ink highlights) won"t move to account for
* the layout.
*
* See also:
*
* * [MergeableMaterial], a piece of material that can split and remerge.
* * [Card], a wrapper for a [Material] of [type] [MaterialType.CARD].
* * <https://material.google.com/>
*
* Creates a piece of material.
*
* The [type], [elevation], [shadowColor], and [animationDuration] arguments
* must not be null.
*
* If a [shape] is specified, then the [borderRadius] property must be
* null and the [type] property must not be [MaterialType.CIRCLE]. If the
* [borderRadius] is specified, then the [type] property must not be
* [MaterialType.CIRCLE]. In both cases, these restrictions are intended to
* catch likely errors.
*/
class Material(
key: Key? = null,
/**
* The kind of material to show (e.g., card or canvas). This
* affects the shape of the widget, the roundness of its corners if
* the shape is rectangular, and the default color.
*/
val type: MaterialType = MaterialType.CANVAS,
/**
* The z-coordinate at which to place this material. This controls the size
* of the shadow below the material.
*
* If this is non-zero, the contents of the material are clipped, because the
* widget conceptually defines an independent printed piece of material.
*
* Defaults to 0. Changing this value will cause the shadow to animate over
* [animationDuration].
*/
val elevation: Float = 0.0f,
/**
* The color to paint the material.
*
* Must be opaque. To create a transparent piece of material, use
* [MaterialType.TRANSPARENCY].
*
* By default, the color is derived from the [type] of material.
*/
val color: Color? = null,
/**
* The color to paint the shadow below the material.
*
* Defaults to fully opaque black.
*/
val shadowColor: Color = Color(0xFF000000.toInt()),
/** The typographical style to use for text within this material. */
val textStyle: TextStyle? = null,
/**
* If non-null, the corners of this box are rounded by this [BorderRadius].
* Otherwise, the corners specified for the current [type] of material are
* used.
*
* If [shape] is non null then the border radius is ignored.
*
* Must be null if [type] is [MaterialType.CIRCLE].
*/
val borderRadius: BorderRadius? = null,
/**
* Defines the material"s shape as well its shadow.
*
* If shape is non null, the [borderRadius] is ignored and the material"s
* clip boundary and shadow are defined by the shape.
*
* A shadow is only displayed if the [elevation] is greater than
* zero.
*/
val shape: ShapeBorder? = null,
/**
* Defines the duration of animated changes for [shape], [elevation],
* and [shadowColor].
*
* The default value is [ThemeChangeDuration].
*/
val animationDuration: Duration = ThemeChangeDuration,
/**
* The widget below this widget in the tree.
*
* {@macro flutter.widgets.child}
*/
val child: Widget? = null
) : StatefulWidget(key) {
init {
assert(!(shape != null && borderRadius != null))
assert(type != MaterialType.CIRCLE && (borderRadius != null || shape != null))
}
override fun createState(): State<out StatefulWidget> = MaterialState(this)
override fun debugFillProperties(properties: DiagnosticPropertiesBuilder) {
super.debugFillProperties(properties)
properties.add(EnumProperty("type", type))
properties.add(FloatProperty.create("elevation", elevation, defaultValue = 0.0f))
properties.add(DiagnosticsProperty.create("color", color, defaultValue = null))
properties.add(
DiagnosticsProperty.create(
"shadowColor",
shadowColor,
defaultValue = Color(0xFF000000.toInt())
)
)
// TODO("Migration|Andrey: Needs TextStyle.debugFillProperties")
// textStyle?.debugFillProperties(properties, prefix= "textStyle.");
properties.add(DiagnosticsProperty.create("shape", shape, defaultValue = null))
properties.add(
DiagnosticsProperty.create(
"borderRadius", borderRadius,
defaultValue = null
)
)
}
}
/**
* The ink controller from the closest instance of this class that
* encloses the given context.
*
* Typical usage is as follows:
*
* ```kotlin
* val inkController = Material(context);
* ```
*/
fun Material(context: BuildContext): RenderInkFeatures {
return context.ancestorRenderObjectOfType(TypeMatcher.create<RenderInkFeatures>())
as RenderInkFeatures
}
private class MaterialState(material: Material) :
TickerProviderStateMixin<Material>(material) {
private val inkFeatureRenderer = GlobalKey.withLabel<MaterialState>("ink renderer")
private fun getBackgroundColor(context: BuildContext): Color? {
if (widget.color != null)
return widget.color
return when (widget.type) {
MaterialType.CANVAS ->
TODO("Migration|Andrey: Needs Theme") // Theme.of(context).canvasColor
MaterialType.CARD ->
TODO("Migration|Andrey: Needs Theme") // Theme.of(context).cardColor
else -> null
}
}
override fun build(context: BuildContext): Widget {
val backgroundColor = getBackgroundColor(context)
assert(backgroundColor != null || widget.type == MaterialType.TRANSPARENCY)
var contents = widget.child
if (contents != null) {
// TODO("Migration|Andrey: needs AnimatedDefaultTextStyle")
// contents = AnimatedDefaultTextStyle (
// style: widget.textStyle ?? Theme.of(context).textTheme.body1,
// duration: widget.animationDuration,
// child: contents
// )
}
// TODO("Migration|Andrey: needs LayoutChangedNotification")
contents = /*NotificationListener < LayoutChangedNotification > (*/
// onNotification: (LayoutChangedNotification notification) {
// final RenderInkFeatures renderer =
// inkFeatureRenderer.currentContext.findRenderObject();
// renderer.didChangeLayout();
// return true;
// },
/*child:*/ InkFeatures(
key = inkFeatureRenderer,
color = backgroundColor,
child = contents!!,
vsync = this
)
// )
// TODO("Migration|Andrey: needs AnimatedPhysicalModel")
// // PhysicalModel has a temporary workaround for a performance issue that
// // speeds up rectangular non transparent material (the workaround is to
// // skip the call to ui.Canvas.saveLayer if the border radius is 0).
// // Until the saveLayer performance issue is resolved, we're keeping this
// // special case here for canvas material type that is using the default
// // shape (RECTANGLE). We could go down this fast path for explicitly
// // specified rectangles (e.g shape RoundedRectangleBorder with radius 0, but
// // we choose not to as we want the change from the fast-path to the
// // slow-path to be noticeable in the construction site of Material.
// if (widget.type == MaterialType.canvas && widget.shape == null && widget.borderRadius == null) {
// return new AnimatedPhysicalModel (
// curve: Curves.fastOutSlowIn,
// duration: widget.animationDuration,
// shape: BoxShape.RECTANGLE,
// borderRadius: BorderRadius.zero,
// elevation: widget.elevation,
// color: backgroundColor,
// shadowColor: widget.shadowColor,
// animateColor: false,
// child: contents,
// );
// }
val shape = getShape()
if (widget.type == MaterialType.TRANSPARENCY)
return transparentInterior(shape = shape, contents = contents)
return MaterialInterior(
curve = Curves.fastOutSlowIn,
duration = widget.animationDuration,
shape = shape,
elevation = widget.elevation,
color = backgroundColor!!,
shadowColor = widget.shadowColor,
child = contents
)
}
/**
* Determines the shape for this Material.
*
* If a shape was specified, it will determine the shape.
* If a borderRadius was specified, the shape is a rounded rectangle.
* Otherwise, the shape is determined by the widget type as described in the
* Material class documentation.
*/
private fun getShape(): ShapeBorder {
if (widget.shape != null)
return widget.shape!!
if (widget.borderRadius != null)
return RoundedRectangleBorder(borderRadius = widget.borderRadius!!)
return when (widget.type) {
MaterialType.CANVAS,
MaterialType.TRANSPARENCY ->
RoundedRectangleBorder()
MaterialType.CARD,
MaterialType.BUTTON ->
RoundedRectangleBorder(
borderRadius = widget.borderRadius ?: MaterialEdges[widget.type]
?: BorderRadius.Zero
)
MaterialType.CIRCLE ->
CircleBorder()
}
}
}
internal fun transparentInterior(shape: ShapeBorder, contents: Widget): Widget {
return ClipPath(
child = ShapeBorderPaint(
child = contents,
shape = shape
),
clipper = ShapeBorderClipper(
shape = shape
)
)
}
private class ShapeBorderPaint(
val child: Widget,
val shape: ShapeBorder
) : StatelessWidget() {
override fun build(context: BuildContext): Widget {
return CustomPaint(
child = child,
foregroundPainter = ShapeBorderPainter(shape, Directionality.of(context))
)
}
}
private class ShapeBorderPainter(
val border: ShapeBorder,
val textDirection: TextDirection
) : CustomPainter() {
override fun paint(canvas: Canvas, size: Size) {
border.paint(canvas, Offset.zero and size, textDirection)
}
override fun shouldRepaint(oldDelegate: CustomPainter) =
(oldDelegate as ShapeBorderPainter).border != border
}
/**
* The interior of non-transparent material.
*
* Animates [elevation], [shadowColor], and [shape].
*/
class MaterialInterior(
key: Key? = null,
/**
* The widget below this widget in the tree.
*
* {@macro flutter.widgets.child}
*/
val child: Widget,
/**
* The border of the widget.
*
* This border will be painted, and in addition the outer path of the border
* determines the physical shape.
*/
val shape: ShapeBorder,
/** The target z-coordinate at which to place this physical object. */
val elevation: Float,
/** The target background color. */
val color: Color,
/** The target shadow color. */
val shadowColor: Color,
curve: Curve = Curves.linear,
duration: Duration
) : ImplicitlyAnimatedWidget(key, curve, duration) {
override fun createState() = MaterialInteriorState(this)
override fun debugFillProperties(properties: DiagnosticPropertiesBuilder) {
super.debugFillProperties(properties)
properties.add(DiagnosticsProperty.create("shape", shape))
properties.add(FloatProperty.create("elevation", elevation))
properties.add(DiagnosticsProperty.create("color", color))
properties.add(DiagnosticsProperty.create("shadowColor", shadowColor))
}
}
class MaterialInteriorState(widget: MaterialInterior) :
AnimatedWidgetBaseState<MaterialInterior>(widget) {
private var elevation: Tween<Float>? = null
private var shadowColor: Tween<Color>? = null
private var border: Tween<ShapeBorder>? = null
override fun forEachTween(visitor: TweenVisitor) {
elevation = visitor(elevation, widget.elevation) { value: Float -> Tween(begin = value) }
shadowColor =
visitor(shadowColor, widget.shadowColor) { value: Color -> Tween(begin = value) }
border = visitor(border, widget.shape) { value: ShapeBorder -> Tween(begin = value) }
}
override fun build(context: BuildContext): Widget {
// TODO("Migration|Andrey: find Kotlin friendly way of creating tweens w/o nulls.")
val shape = border!!.evaluate(animation!!)
return PhysicalShape(
child = ShapeBorderPaint(
child = widget.child,
shape = shape
),
clipper = ShapeBorderClipper(
shape = shape,
textDirection = Directionality.of(context)
),
elevation = elevation!!.evaluate(animation!!),
color = widget.color,
shadowColor = shadowColor!!.evaluate(animation!!)
)
}
}
class RenderInkFeatures(
child: RenderBox? = null,
/**
* This class should exist in a 1:1 relationship with a MaterialState object,
* since there's no current support for dynamically changing the ticker
* provider.
*/
override val vsync: TickerProvider,
/**
* This is here to satisfy the MaterialInkController contract.
* The actual painting of this color is done by a Container in the
* MaterialState build method.
*/
override var color: Color?
) : RenderProxyBox(child), MaterialInkController {
private var inkFeatures: MutableList<InkFeature>? = null
override fun addInkFeature(feature: InkFeature) {
assert(!feature.debugDisposed)
assert(feature.controller == this)
inkFeatures = inkFeatures ?: mutableListOf()
assert(!inkFeatures!!.contains(feature))
inkFeatures!!.add(feature)
markNeedsPaint()
}
internal fun removeFeature(feature: InkFeature) {
assert(inkFeatures != null)
inkFeatures!!.remove(feature)
markNeedsPaint()
}
internal fun didChangeLayout() {
if (inkFeatures != null && inkFeatures!!.isNotEmpty()) {
markNeedsPaint()
}
}
override fun hitTestSelf(position: Offset) = true
override fun paint(context: PaintingContext, offset: Offset) {
val inkFeatures = inkFeatures
if (inkFeatures != null && inkFeatures.isNotEmpty()) {
val canvas = context.canvas
canvas.save()
canvas.translate(offset.dx, offset.dy)
canvas.clipRect(Offset.zero and size)
inkFeatures.forEach { it.paint(canvas) }
canvas.restore()
}
super.paint(context, offset)
}
}
internal class InkFeatures(
key: Key?,
val color: Color?,
val vsync: TickerProvider,
child: Widget
) : SingleChildRenderObjectWidget(key, child) {
// This widget must be owned by a MaterialState, which must be provided as the vsync.
// This relationship must be 1:1 and cannot change for the lifetime of the MaterialState.
override fun createRenderObject(context: BuildContext): RenderObject {
return RenderInkFeatures(
color = color,
vsync = vsync
)
}
override fun updateRenderObject(context: BuildContext, renderObject: RenderObject) {
(renderObject as RenderInkFeatures).also {
it.color = color
assert(vsync == it.vsync)
}
}
}
/**
* A visual reaction on a piece of [Material].
*
* To add an ink feature to a piece of [Material], obtain the
* [MaterialInkController] via [Material.of] and call
* [MaterialInkController.addInkFeature].
*/
abstract class InkFeature constructor(
/**
* The [MaterialInkController] associated with this [InkFeature].
*
* Typically used by subclasses to call
* [MaterialInkController.markNeedsPaint] when they need to repaint.
*/
controller: RenderInkFeatures,
/** The render box whose visual position defines the frame of reference for this ink feature. */
val referenceBox: RenderBox,
/** Called when the ink feature is no longer visible on the material. */
val onRemoved: VoidCallback? = null
) {
// TODO("Migration|Andrey: Try to merge MaterialInkController with RenderInkFeatures and remove
// the backing field after it.")
private var _controller: RenderInkFeatures = controller
val controller: MaterialInkController get() = _controller
internal var debugDisposed = false
/** Free up the resources associated with this ink feature. */
@CallSuper
open fun dispose() {
assert(!debugDisposed)
assert { debugDisposed = true; true; }
_controller.removeFeature(this)
onRemoved?.invoke()
}
internal fun paint(canvas: Canvas) {
assert(referenceBox.attached)
assert(!debugDisposed)
// find the chain of renderers from us to the feature's referenceBox
val descendants = mutableListOf(referenceBox)
var node = referenceBox
while (node != _controller) {
node = node.parent as RenderBox
descendants.add(node)
}
// determine the transform that gets our coordinate system to be like theirs
val transform = Matrix4.identity()
assert(descendants.size >= 2)
for (index in descendants.size - 1 downTo 1) {
descendants[index].applyPaintTransform(descendants[index - 1], transform)
}
paintFeature(canvas, transform)
}
/**
* Override this method to paint the ink feature.
*
* The transform argument gives the coordinate conversion from the coordinate
* system of the canvas to the coordinate system of the [referenceBox].
*/
protected abstract fun paintFeature(canvas: Canvas, transform: Matrix4)
}