/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.react.views.textinput; import javax.annotation.Nullable; import java.util.Map; import android.graphics.PorterDuff; import android.os.SystemClock; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.TextView; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.JSApplicationCausedNativeException; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.BaseViewManager; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactProp; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.UIProp; import com.facebook.react.uimanager.ViewDefaults; import com.facebook.react.uimanager.ViewProps; import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.views.text.DefaultStyleValuesUtil; /** * Manages instances of TextInput. */ public class ReactTextInputManager extends BaseViewManager<ReactEditText, ReactTextInputShadowNode> { /* package */ static final String REACT_CLASS = "AndroidTextInput"; private static final int FOCUS_TEXT_INPUT = 1; private static final int BLUR_TEXT_INPUT = 2; private static final String KEYBOARD_TYPE_EMAIL_ADDRESS = "email-address"; private static final String KEYBOARD_TYPE_NUMERIC = "numeric"; @Override public String getName() { return REACT_CLASS; } @Override public ReactEditText createViewInstance(ThemedReactContext context) { ReactEditText editText = new ReactEditText(context); int inputType = editText.getInputType(); editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE)); editText.setImeOptions(EditorInfo.IME_ACTION_DONE); editText.setTextSize( TypedValue.COMPLEX_UNIT_PX, (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))); return editText; } @Override public ReactTextInputShadowNode createShadowNodeInstance() { return new ReactTextInputShadowNode(); } @Override public Class<ReactTextInputShadowNode> getShadowNodeClass() { return ReactTextInputShadowNode.class; } @Nullable @Override public Map<String, Object> getExportedCustomBubblingEventTypeConstants() { return MapBuilder.<String, Object>builder() .put( "topSubmitEditing", MapBuilder.of( "phasedRegistrationNames", MapBuilder.of( "bubbled", "onSubmitEditing", "captured", "onSubmitEditingCapture"))) .put( "topEndEditing", MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", "onEndEditing", "captured", "onEndEditingCapture"))) .put( "topTextInput", MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", "onTextInput", "captured", "onTextInputCapture"))) .put( "topFocus", MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", "onFocus", "captured", "onFocusCapture"))) .put( "topBlur", MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture"))) .build(); } @Override public @Nullable Map<String, Integer> getCommandsMap() { return MapBuilder.of("focusTextInput", FOCUS_TEXT_INPUT, "blurTextInput", BLUR_TEXT_INPUT); } @Override public void receiveCommand( ReactEditText reactEditText, int commandId, @Nullable ReadableArray args) { switch (commandId) { case FOCUS_TEXT_INPUT: reactEditText.requestFocusFromJS(); break; case BLUR_TEXT_INPUT: reactEditText.clearFocusFromJS(); break; } } @Override public void updateExtraData(ReactEditText view, Object extraData) { if (extraData instanceof float[]) { float[] padding = (float[]) extraData; view.setPadding( (int) Math.ceil(padding[0]), (int) Math.ceil(padding[1]), (int) Math.ceil(padding[2]), (int) Math.ceil(padding[3])); } else if (extraData instanceof ReactTextUpdate) { view.maybeSetText((ReactTextUpdate) extraData); } } @ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = ViewDefaults.FONT_SIZE_SP) public void setFontSize(ReactEditText view, float fontSize) { view.setTextSize( TypedValue.COMPLEX_UNIT_PX, (int) Math.ceil(PixelUtil.toPixelFromSP(fontSize))); } @ReactProp(name = "placeholder") public void setPlaceholder(ReactEditText view, @Nullable String placeholder) { view.setHint(placeholder); } @ReactProp(name = "placeholderTextColor", customType = "Color") public void setPlaceholderTextColor(ReactEditText view, @Nullable Integer color) { if (color == null) { view.setHintTextColor(DefaultStyleValuesUtil.getDefaultTextColorHint(view.getContext())); } else { view.setHintTextColor(color); } } @ReactProp(name = "underlineColorAndroid", customType = "Color") public void setUnderlineColor(ReactEditText view, @Nullable Integer underlineColor) { if (underlineColor == null) { view.getBackground().clearColorFilter(); } else { view.getBackground().setColorFilter(underlineColor, PorterDuff.Mode.SRC_IN); } } @ReactProp(name = "textAlign") public void setTextAlign(ReactEditText view, int gravity) { view.setGravityHorizontal(gravity); } @ReactProp(name = "textAlignVertical") public void setTextAlignVertical(ReactEditText view, int gravity) { view.setGravityVertical(gravity); } @ReactProp(name = "editable", defaultBoolean = true) public void setEditable(ReactEditText view, boolean editable) { view.setEnabled(editable); } @ReactProp(name = ViewProps.NUMBER_OF_LINES, defaultInt = 1) public void setNumLines(ReactEditText view, int numLines) { view.setLines(numLines); } @ReactProp(name = "autoCorrect") public void setAutoCorrect(ReactEditText view, @Nullable Boolean autoCorrect) { // clear auto correct flags, set SUGGESTIONS or NO_SUGGESTIONS depending on value updateStagedInputTypeFlag( view, InputType.TYPE_TEXT_FLAG_AUTO_CORRECT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS, autoCorrect != null ? (autoCorrect.booleanValue() ? InputType.TYPE_TEXT_FLAG_AUTO_CORRECT : InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) : 0); } @ReactProp(name = "multiline", defaultBoolean = false) public void setMultiline(ReactEditText view, boolean multiline) { updateStagedInputTypeFlag( view, multiline ? 0 : InputType.TYPE_TEXT_FLAG_MULTI_LINE, multiline ? InputType.TYPE_TEXT_FLAG_MULTI_LINE : 0); } @ReactProp(name = "password", defaultBoolean = false) public void setPassword(ReactEditText view, boolean password) { updateStagedInputTypeFlag( view, password ? 0 : InputType.TYPE_TEXT_VARIATION_PASSWORD, password ? InputType.TYPE_TEXT_VARIATION_PASSWORD : 0); } @ReactProp(name = "autoCapitalize", defaultInt = InputType.TYPE_CLASS_TEXT) public void setAutoCapitalize(ReactEditText view, int autoCapitalize) { int flagsToSet = 0; switch (autoCapitalize) { case InputType.TYPE_TEXT_FLAG_CAP_SENTENCES: case InputType.TYPE_TEXT_FLAG_CAP_WORDS: case InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS: case InputType.TYPE_CLASS_TEXT: flagsToSet = autoCapitalize; break; default: throw new JSApplicationCausedNativeException("Invalid autoCapitalize value: " + autoCapitalize); } updateStagedInputTypeFlag( view, InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_CAP_WORDS | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS, flagsToSet); } @ReactProp(name = "keyboardType") public void setKeyboardType(ReactEditText view, @Nullable String keyboardType) { int flagsToSet = 0; if (KEYBOARD_TYPE_NUMERIC.equalsIgnoreCase(keyboardType)) { flagsToSet = InputType.TYPE_CLASS_NUMBER; } else if (KEYBOARD_TYPE_EMAIL_ADDRESS.equalsIgnoreCase(keyboardType)) { flagsToSet = InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; } updateStagedInputTypeFlag( view, InputType.TYPE_CLASS_NUMBER | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, flagsToSet); } @Override protected void onAfterUpdateTransaction(ReactEditText view) { super.onAfterUpdateTransaction(view); view.commitStagedInputType(); } private static void updateStagedInputTypeFlag( ReactEditText view, int flagsToUnset, int flagsToSet) { view.setStagedInputType((view.getStagedInputType() & ~flagsToUnset) | flagsToSet); } private class ReactTextInputTextWatcher implements TextWatcher { private EventDispatcher mEventDispatcher; private ReactEditText mEditText; private String mPreviousText; public ReactTextInputTextWatcher( final ReactContext reactContext, final ReactEditText editText) { mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); mEditText = editText; mPreviousText = null; } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { // Incoming charSequence gets mutated before onTextChanged() is invoked mPreviousText = s.toString(); } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // Rearranging the text (i.e. changing between singleline and multiline attributes) can // also trigger onTextChanged, call the event in JS only when the text actually changed if (count > 0 || before > 0) { Assertions.assertNotNull(mPreviousText); int contentWidth = mEditText.getWidth(); int contentHeight = mEditText.getHeight(); // Use instead size of text content within EditText when available if (mEditText.getLayout() != null) { contentWidth = mEditText.getCompoundPaddingLeft() + mEditText.getLayout().getWidth() + mEditText.getCompoundPaddingRight(); contentHeight = mEditText.getCompoundPaddingTop() + mEditText.getLayout().getHeight() + mEditText.getCompoundPaddingTop(); } // The event that contains the event counter and updates it must be sent first. // TODO: t7936714 merge these events mEventDispatcher.dispatchEvent( new ReactTextChangedEvent( mEditText.getId(), SystemClock.uptimeMillis(), s.toString(), (int) PixelUtil.toDIPFromPixel(contentWidth), (int) PixelUtil.toDIPFromPixel(contentHeight), mEditText.incrementAndGetEventCounter())); mEventDispatcher.dispatchEvent( new ReactTextInputEvent( mEditText.getId(), SystemClock.uptimeMillis(), count > 0 ? s.toString().substring(start, start + count) : "", before > 0 ? mPreviousText.substring(start, start + before) : "", start, count > 0 ? start + count - 1 : start + before)); } } @Override public void afterTextChanged(Editable s) { } } @Override protected void addEventEmitters( final ThemedReactContext reactContext, final ReactEditText editText) { editText.addTextChangedListener(new ReactTextInputTextWatcher(reactContext, editText)); editText.setOnFocusChangeListener( new View.OnFocusChangeListener() { public void onFocusChange(View v, boolean hasFocus) { EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); if (hasFocus) { eventDispatcher.dispatchEvent( new ReactTextInputFocusEvent( editText.getId(), SystemClock.uptimeMillis())); } else { eventDispatcher.dispatchEvent( new ReactTextInputBlurEvent( editText.getId(), SystemClock.uptimeMillis())); eventDispatcher.dispatchEvent( new ReactTextInputEndEditingEvent( editText.getId(), SystemClock.uptimeMillis(), editText.getText().toString())); } } }); editText.setOnEditorActionListener( new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent keyEvent) { // Any 'Enter' action will do if ((actionId & EditorInfo.IME_MASK_ACTION) > 0 || actionId == EditorInfo.IME_NULL) { EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); eventDispatcher.dispatchEvent( new ReactTextInputSubmitEditingEvent( editText.getId(), SystemClock.uptimeMillis(), editText.getText().toString())); } return false; } }); } @Override public @Nullable Map getExportedViewConstants() { return MapBuilder.of( "TextAlign", MapBuilder.of( "start", Gravity.START, "center", Gravity.CENTER_HORIZONTAL, "end", Gravity.END), "TextAlignVertical", MapBuilder.of( "top", Gravity.TOP, "center", Gravity.CENTER_VERTICAL, "bottom", Gravity.BOTTOM)); } }