[go: nahoru, domu]

blob: 8422f4a340bbe757221b3e6a9330d85342f556db [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.core
import androidx.compose.Composable
import androidx.compose.mutableStateOf
import androidx.compose.onDispose
import androidx.compose.remember
import androidx.compose.state
import androidx.ui.core.gesture.DragObserver
import androidx.ui.core.gesture.PressGestureDetector
import androidx.ui.core.gesture.TouchSlopDragGestureDetector
import androidx.ui.core.input.FocusManager
import androidx.ui.input.EditProcessor
import androidx.ui.input.ImeAction
import androidx.ui.input.InputState
import androidx.ui.input.KeyboardType
import androidx.ui.input.NO_SESSION
import androidx.ui.input.VisualTransformation
import androidx.ui.semantics.Semantics
import androidx.ui.semantics.onClick
import androidx.ui.text.TextDelegate
import androidx.ui.text.TextFieldValue
import androidx.ui.text.TextLayoutResult
import androidx.ui.text.TextRange
import androidx.ui.text.TextStyle
import androidx.ui.unit.PxPosition
/**
* A user interface element for entering and modifying text.
*
* The TextField component renders an input and additional decorations set by input service
* which is software keyboard in Android. Once input service modify the text, you will get callback
* [onValueChange] with new text. Then, you can set this new text so that this component renders
* up-to-date text from input service.
*
* Example usage:
* @sample androidx.ui.framework.samples.StringTextFieldSample
*
* This is the most simple TextField that observes only text update and have control only for the
* text. If you want to change/observe the selection/cursor location, you can use TextField with
* [TextFieldValue] object.
*
* Note: Please be careful if you setting text other than the one passed to [onValueChange]
* callback. Especially, it is not recommended to modify the text passed to [onValueChange]
* callback. The text change may be translated to full context reset by input service and end up
* with input session restart. This will be visible to users, for example, any ongoing composition
* text will be cleared or committed, then software keyboard may go back to the default one.
*
* @param value The text to be shown in the [TextField]. If you want to specify cursor location or
* selection range, use [TextField] with [TextFieldValue] instead.
* @param onValueChange Called when the input service updates the text. When the input service
* update the text, this callback is called with the updated text. If you want to observe the cursor
* location or selection range, use [TextField] with [TextFieldValue] instead.
* @param textStyle Style configuration that applies at character level such as color, font etc.
* @param keyboardType The keyboard type to be used in this text field. Note that this input type
* is honored by IME and shows corresponding keyboard but this is not guaranteed. For example,
* some IME may send non-ASCII character even if you set [KeyboardType.Ascii].
* @param imeAction The IME action. This IME action is honored by IME and may show specific icons
* on the keyboard. For example, search icon may be shown if [ImeAction.Search] is specified.
* Then, when user tap that key, the [onImeActionPerformed] callback is called with specified
* ImeAction.
* @param onFocus Called when the input field gains focus.
* @param onBlur Called when the input field loses focus.
* @param focusIdentifier Optional value to identify focus identifier. You can pass
* [FocusManager.requestFocus] to this value to move focus to this TextField. This identifier
* must be unique in your app. If you have duplicated identifiers, the behavior is undefined.
* @param onImeActionPerformed Called when the input service requested an IME action. When the
* input service emitted an IME action, this callback is called with the emitted IME action. Note
* that this IME action may be different from what you specified in [imeAction].
* @param visualTransformation Optional visual filter for changing visual output of input field.
*
* @see PasswordTextField
* @see TextFieldValue
* @see ImeAction
* @see KeyboardType
* @see VisualTransformation
*/
@Composable
fun TextField(
value: String,
modifier: Modifier = Modifier.None,
onValueChange: (String) -> Unit = {},
textStyle: TextStyle? = null,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Unspecified,
onFocus: () -> Unit = {},
onBlur: () -> Unit = {},
focusIdentifier: String? = null,
onImeActionPerformed: (ImeAction) -> Unit = {},
visualTransformation: VisualTransformation? = null
) {
val fullModel = state { InputState() }
if (fullModel.value.text != value) {
val newSelection = TextRange(
fullModel.value.selection.start.coerceIn(0, value.length),
fullModel.value.selection.end.coerceIn(0, value.length)
)
fullModel.value = InputState(
text = value,
selection = newSelection
)
}
BaseTextField(
value = fullModel.value,
modifier = modifier,
onValueChange = {
val prevValue = fullModel.value.text
fullModel.value = it
if (prevValue != it.text) {
onValueChange(it.text)
}
},
textStyle = textStyle,
keyboardType = keyboardType,
imeAction = imeAction,
onFocus = onFocus,
onBlur = onBlur,
focusIdentifier = focusIdentifier,
onImeActionPerformed = onImeActionPerformed,
visualTransformation = visualTransformation
)
}
/**
* A user interface element for entering and modifying text.
*
* The TextField component renders an input and additional decorations set by input service
* which is software keyboard in Android. Once input service modify the text, you will get callback
* [onValueChange] with new text. Then, you can set this new text so that this component renders
* up-to-date text from input service.
*
* Example usage:
* @sample androidx.ui.framework.samples.EditorModelTextFieldSample
*
* Note: Please be careful if you setting model other than the one passed to [onValueChange]
* callback including selection or cursor. Especially, it is not recommended to modify the model
* passed to [onValueChange] callback. Any change to text, selection or cursor may be translated to
* full context reset by input service and end up with input session restart. This will be visible
* to users, for example, any ongoing composition text will be cleared or committed, then software
* keyboard may go back to the default one.
*
* @param value The [TextFieldValue] to be shown in the [TextField].
* @param onValueChange Called when the input service updates the text, selection or cursor. When
* the input service update the text, selection or cursor, this callback is called with the updated
* [TextFieldValue]. If you want to observe the composition text, use [TextField] with
* compositionRange instead.
* @param textStyle Style configuration that applies at character level such as color, font etc.
* @param keyboardType The keyboard type to be used in this text field. Note that this input type
* is honored by IME and shows corresponding keyboard but this is not guaranteed. For example,
* some IME may send non-ASCII character even if you set [KeyboardType.Ascii].
* @param imeAction The IME action. This IME action is honored by IME and may show specific icons
* on the keyboard. For example, search icon may be shown if [ImeAction.Search] is specified.
* Then, when user tap that key, the [onImeActionPerformed] callback is called with specified
* ImeAction.
* @param onFocus Called when the input field gains focus.
* @param onBlur Called when the input field loses focus.
* @param focusIdentifier Optional value to identify focus identifier. You can pass
* [FocusManager.requestFocus] to this value to move focus to this TextField. This identifier
* must be unique in your app. If you have duplicated identifiers, the behavior is undefined.
* @param onImeActionPerformed Called when the input service requested an IME action. When the
* input service emitted an IME action, this callback is called with the emitted IME action. Note
* that this IME action may be different from what you specified in [imeAction].
* @param visualTransformation Optional visual filter for changing visual output of input field.
*
* @see TextFieldValue
* @see ImeAction
* @see KeyboardType
* @see VisualTransformation
*/
@Composable
fun TextField(
value: TextFieldValue,
modifier: Modifier = Modifier.None,
onValueChange: (TextFieldValue) -> Unit = {},
textStyle: TextStyle? = null,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Unspecified,
onFocus: () -> Unit = {},
onBlur: () -> Unit = {},
focusIdentifier: String? = null,
onImeActionPerformed: (ImeAction) -> Unit = {},
visualTransformation: VisualTransformation? = null
) {
val fullModel = state { InputState() }
if (fullModel.value.text != value.text || fullModel.value.selection != value.selection) {
val newSelection = TextRange(
value.selection.start.coerceIn(0, value.text.length),
value.selection.end.coerceIn(0, value.text.length)
)
fullModel.value = InputState(
text = value.text,
selection = newSelection
)
}
BaseTextField(
value = fullModel.value,
modifier = modifier,
onValueChange = {
val prevState = fullModel.value
fullModel.value = it
if (prevState.text != it.text || prevState.selection != it.selection) {
onValueChange(TextFieldValue(it.text, it.selection))
}
},
textStyle = textStyle,
keyboardType = keyboardType,
imeAction = imeAction,
onFocus = onFocus,
onBlur = onBlur,
focusIdentifier = focusIdentifier,
onImeActionPerformed = onImeActionPerformed,
visualTransformation = visualTransformation
)
}
/**
* A user interface element for entering and modifying text.
*
* The TextField component renders an input and additional decorations set by input service
* which is software keyboard in Android. Once input service modify the text, you will get callback
* [onValueChange] with new text. Then, you can set this new text so that this component renders
* up-to-date text from input service.
*
* Example usage:
* @sample androidx.ui.framework.samples.CompositionEditorModelTextFieldSample
*
* It is not recommended to use this component unless you are interested in composition region.
* The composition text is set by input service and you don't have control of it. If you modify
* composition, the input service may confuse and restart new input session. Also please do not
* expect no composition range at the beginning of input session. The input service may convert
* existing text to composition text at the beginning of the input session.
*
* Note: Please be careful if you setting model other than the one passed to [onValueChange]
* callback including selection or cursor. Especially, it is not recommended to modify the model
* passed to [onValueChange] callback. Any change to text, selection or cursor may be translated to
* full context reset by input service and end up with input session restart. This will be visible
* to users, for example, any ongoing composition text will be cleared or committed, then software
* keyboard may go back to the default one.
*
* @param model The [TextFieldValue] to be shown in the [TextField].
* @param onValueChange Called when the input service updates the text, selection or cursor. When
* the input service update the text, selection or cursor, this callback is called with the updated
* [TextFieldValue].
* @param textStyle Style configuration that applies at character level such as color, font etc.
* @param keyboardType The keyboard type to be used in this text field. Note that this input type
* is honored by IME and shows corresponding keyboard but this is not guaranteed. For example,
* some IME may send non-ASCII character even if you set [KeyboardType.Ascii].
* @param imeAction The IME action. This IME action is honored by IME and may show specific icons
* on the keyboard. For example, search icon may be shown if [ImeAction.Search] is specified.
* Then, when user tap that key, the [onImeActionPerformed] callback is called with specified
* ImeAction.
* @param onFocus Called when the input field gains focus.
* @param onBlur Called when the input field loses focus.
* @param focusIdentifier Optional value to identify focus identifier. You can pass
* [FocusManager.requestFocus] to this value to move focus to this TextField. This identifier
* must be unique in your app. If you have duplicated identifiers, the behavior is undefined.
* @param onImeActionPerformed Called when the input service requested an IME action. When the
* input service emitted an IME action, this callback is called with the emitted IME action. Note
* that this IME action may be different from what you specified in [imeAction].
* @param visualTransformation Optional visual filter for changing visual output of input field.
*
* @see TextFieldValue
* @see ImeAction
* @see KeyboardType
* @see VisualTransformation
*/
@Composable
fun TextField(
model: TextFieldValue,
compositionRange: TextRange?,
modifier: Modifier = Modifier.None,
onValueChange: (TextFieldValue, TextRange?) -> Unit = { _, _ -> },
textStyle: TextStyle? = null,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Unspecified,
onFocus: () -> Unit = {},
onBlur: () -> Unit = {},
focusIdentifier: String? = null,
onImeActionPerformed: (ImeAction) -> Unit = {},
visualTransformation: VisualTransformation? = null
) {
BaseTextField(
value = InputState(model.text, model.selection, compositionRange),
modifier = modifier,
onValueChange = { onValueChange(TextFieldValue(it.text, it.selection), it.composition) },
textStyle = textStyle,
keyboardType = keyboardType,
imeAction = imeAction,
onFocus = onFocus,
onBlur = onBlur,
focusIdentifier = focusIdentifier,
onImeActionPerformed = onImeActionPerformed,
visualTransformation = visualTransformation
)
}
/**
* The common TextField implementation.
*/
@Composable
internal fun BaseTextField(
value: InputState,
modifier: Modifier,
onValueChange: (InputState) -> Unit = { },
textStyle: TextStyle? = null,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Unspecified,
onFocus: () -> Unit = {},
onBlur: () -> Unit = {},
focusIdentifier: String? = null,
onImeActionPerformed: (ImeAction) -> Unit = {},
visualTransformation: VisualTransformation? = null
) {
// If developer doesn't pass new value to TextField, recompose won't happen but internal state
// and IME may think it is updated. To fix this inconsistent state, enforce recompose by
// incrementing generation counter when we callback to the developer and reset the state with
// the latest state.
val generation = state { 0 }
val Wrapper: @Composable() (Int, @Composable() () -> Unit) -> Unit = { _, child -> child() }
val onValueChangeWrapper: (InputState) -> Unit = { onValueChange(it); generation.value++ }
Wrapper(generation.value) {
// Ambients
val style = CurrentTextStyleAmbient.current
val textInputService = TextInputServiceAmbient.current
val density = DensityAmbient.current
val resourceLoader = FontLoaderAmbient.current
val layoutDirection = LayoutDirectionAmbient.current
// State
val mergedStyle = style.merge(textStyle)
val (visualText, offsetMap) = remember(value, visualTransformation) {
val transformed = TextFieldDelegate.applyVisualFilter(value, visualTransformation)
value.composition?.let {
TextFieldDelegate.applyCompositionDecoration(it, transformed)
} ?: transformed
}
val state = remember {
TextFieldState(
TextDelegate(
text = visualText,
style = mergedStyle,
density = density,
resourceLoader = resourceLoader,
layoutDirection = layoutDirection
)
)
}
state.textDelegate = updateTextDelegate(
current = state.textDelegate,
text = visualText,
style = mergedStyle,
density = density,
resourceLoader = resourceLoader,
layoutDirection = layoutDirection
)
state.processor.onNewState(value, textInputService, state.inputSession)
TextInputEventObserver(
focusIdentifier = focusIdentifier,
onPress = { },
onFocus = {
state.hasFocus = true
state.inputSession = TextFieldDelegate.onFocus(
textInputService,
value,
state.processor,
keyboardType,
imeAction,
onValueChangeWrapper,
onImeActionPerformed)
state.layoutCoordinates?.let { coords ->
textInputService?.let { textInputService ->
state.layoutResult?.let { layoutResult ->
TextFieldDelegate.notifyFocusedRect(
value,
state.textDelegate,
layoutResult,
coords,
textInputService,
state.inputSession,
state.hasFocus,
offsetMap
)
}
}
}
onFocus()
},
onBlur = {
state.hasFocus = false
TextFieldDelegate.onBlur(
textInputService,
state.inputSession,
state.processor,
onValueChangeWrapper)
onBlur()
},
onRelease = {
state.layoutResult?.let { layoutResult ->
TextFieldDelegate.onRelease(
it,
layoutResult,
state.processor,
offsetMap,
onValueChangeWrapper,
textInputService,
state.inputSession,
state.hasFocus
)
}
}
) {
val textDrawModifier = draw { canvas, _ ->
state.layoutResult?.let { layoutResult ->
TextFieldDelegate.draw(
canvas,
value,
offsetMap,
layoutResult,
state.hasFocus,
DefaultSelectionColor
)
}
}
Layout(
modifier = modifier + textDrawModifier,
children = @Composable {
OnPositioned {
if (textInputService != null) {
state.layoutCoordinates = it
state.layoutResult?.let { layoutResult ->
TextFieldDelegate.notifyFocusedRect(
value,
state.textDelegate,
layoutResult,
it,
textInputService,
state.inputSession,
state.hasFocus,
offsetMap
)
}
}
}
},
measureBlock = { _, constraints, _ ->
TextFieldDelegate.layout(
state.textDelegate,
constraints,
state.layoutResult
).let { (width, height, result) ->
state.layoutResult = result
layout(width, height) {}
}
}
)
}
}
}
private class TextFieldState(
var textDelegate: TextDelegate
) {
val processor = EditProcessor()
var inputSession = NO_SESSION
/**
* This should be a state as every time we update the value we need to redraw it.
* @Model observation during onDraw callback will make it work.
*/
var hasFocus by mutableStateOf(false)
/** The last layout coordinates for the Text's layout, used by selection */
var layoutCoordinates: LayoutCoordinates? = null
/** The latest TextLayoutResult calculated in the measure block */
var layoutResult: TextLayoutResult? = null
}
/**
* Helper composable for observing all text input related events.
*/
@Composable
private fun TextInputEventObserver(
onPress: (PxPosition) -> Unit,
onRelease: (PxPosition) -> Unit,
onFocus: () -> Unit,
onBlur: () -> Unit,
focusIdentifier: String?,
children: @Composable() () -> Unit
) {
val focused = state { false }
val focusManager = FocusManagerAmbient.current
val focusNode = remember {
val node = object : FocusManager.FocusNode {
override fun onFocus() {
onFocus()
focused.value = true
}
override fun onBlur() {
onBlur()
focused.value = false
}
}
if (focusIdentifier != null)
focusManager.registerFocusNode(focusIdentifier, node)
node
}
onDispose {
if (focusIdentifier != null)
focusManager.unregisterFocusNode(focusIdentifier)
}
val doFocusIn = {
if (!focused.value) {
focusManager.requestFocus(focusNode)
}
}
Semantics(
properties = {
onClick(action = doFocusIn)
}
) {
DragPositionGestureDetector(
onPress = {
if (focused.value) {
onPress(it)
} else {
doFocusIn()
}
},
onRelease = onRelease,
children = children
)
}
}
/**
* Helper class for tracking dragging event.
*/
internal class DragEventTracker {
private var origin = PxPosition.Origin
private var distance = PxPosition.Origin
/**
* Restart the tracking from given origin.
*
* @param origin The origin of the drag gesture.
*/
fun init(origin: PxPosition) {
this.origin = origin
}
/**
* Pass distance parameter called by DragGestureDetector$onDrag callback
*
* @param distance The distance from the origin of the drag origin.
*/
fun onDrag(distance: PxPosition) {
this.distance = distance
}
/**
* Returns the current position.
*
* @return The position of the current drag point.
*/
fun getPosition(): PxPosition {
return origin + distance
}
}
/**
* Helper composable for tracking drag position.
*/
@Composable
private fun DragPositionGestureDetector(
onPress: (PxPosition) -> Unit,
onRelease: (PxPosition) -> Unit,
children: @Composable() () -> Unit
) {
val tracker = state { DragEventTracker() }
PressGestureDetector(
onPress = {
tracker.value.init(it)
onPress(it)
},
onRelease = { onRelease(tracker.value.getPosition()) }
) {
TouchSlopDragGestureDetector(
dragObserver = object : DragObserver {
override fun onDrag(dragDistance: PxPosition): PxPosition {
tracker.value.onDrag(dragDistance)
return tracker.value.getPosition()
}
}, children = children)
}
}