串行(类似TTY),与(丰富)文本视图的同步用户交互

时间:2016-12-15 04:51:34

标签: android textview

我想使用像TextView这样的东西来做串行/ TTY /文本终端,同步用户交互。我的意思是我需要以下操作:

  • 在最后添加一些新文本(使用各种Span s样式)
  • 同步等待用户在最后输入一些其他文本(用一些Span s样式),在虚拟键盘上用换行符/一些“提交”式按钮完成/等等。没有其他部分文本应该是可选择的,可编辑的等等。

当然第二个操作在UI线程上没有任何意义,所以我想象的方式,我有一个单独的线程,具有表单的同步代码

// NOT on the UI thread here
textui.addText("What's your name? ", 
    new StyleSpan(Typeface.BOLD), 
    new ForegroundColorSpan(Color.RED));
String name = textui.getLine(
    new BackgroundColorSpan(Color.GREEN),
    new StyleSpan(Typeface.ITALIC));
textui.addText("Hello, ");
textui.addText(name, 
    new StyleSpan(Typeface.BOLD));
textui.addText("!");

请注意,getLine应该等待用户输入,用户应该无法编辑What's your name?部分,并且当用户输入时,他的输入应该是绿色斜体。这是一些模型:

  1. 添加第一行后:

    1

  2. 用户开始输入

    2

  3. 用户提交了他的输入

    3

  4. 我尝试过的一件事是使用带有EditText的{​​{1}}拒绝用户修改,直到调用InputFilter并禁止编辑任何以前的输出,但这仍然允许用户移动光标并选择输出,就像输入一样。

    编辑添加:由于我的问题“不清楚”,我得到了一个接近的投票,所以让我试着用另一种方式来解释。想象一下,你有一个使用getLineSystem.out.println的控制台程序,你希望在Android控件中复制那个确切的用户体验(可能被其他Android控件包围),并增加了样式。

1 个答案:

答案 0 :(得分:1)

更新:添加了允许/禁止编辑的标志,并指定字段是单行还是多行。已将自定义EditText移至单独的类文件。固定光标移动问题。

这是一种可能适合您的方法。我知道您正在寻找TTY类型的功能。我不确定同步部分是关于什么的,但是这里有。

当禁止编辑时,方法是使用自定义EditText来消除用户在字段中移动的能力,并从硬件键盘中检测输入密钥(字段终止)和禁止删除键。这完全由EditText中定义的自定义TtyEditText.java完成。

对屏幕键盘的控制工作方式略有不同,并使用TextWatcher来处理事情。

示例应用程序只需循环显示三个提示,并使用您在问题中提到的Spans类型显示用户输入屏幕的信息。

这是一个不允许编辑的快速视频。

Demo video

使用左右箭头键在一个字段中移动有一个怪癖。右箭头键工作正常,但左箭头键似乎想要在字段之间移动而不是在字段内返回。我已明确禁用两者,直到找到解决方案,即使这是一个问题。

根据this page

  

用户还可以使用键盘上的箭头键导航您的应用程序(行为与使用D-pad或轨迹球导航时的行为相同)。系统根据屏幕上视图的布局,提供关于哪个视图应该在给定方向上聚焦的最佳猜测。但是,有时系统可能猜错了。

那么,Android猜错了吗?这已通过使完成的字段不可聚焦来解决。代码已更正为处理光标键。

<强> MainActivity.java

public class MainActivity extends AppCompatActivity
        implements TextWatcher, TtyEditText.FieldAction {
    private String mPrompts[] = new String[]{
            "What is your name? ",
            "What is your quest? ",
            "What is the air-speed velocity of an unladen swallow? "
    };
    private int mPromptIndex = -1;
    private LinearLayout mLinearLayout;
    private TtyEditText mCurrentField;

    // true-current field can be edited (backspace/selection); false-disallow field editing
    private final boolean mAllowEditing = true;
    // true input is single line EditText else multi-line
    private final boolean mSingleLine = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        final ScrollView scrollView;

        super.onCreate(savedInstanceState);
        scrollView = createViewGroup(); // Simple LinearLayout within ScrollView
        mLinearLayout = (LinearLayout) scrollView.getChildAt(0);
        setContentView(scrollView);

        // Get the ball rolling with first prompt.
        mCurrentField = generatePrompt(mLinearLayout);
    }

    // Create the next prompt field and place it at the bottom of the layout.
    private TtyEditText generatePrompt(LinearLayout layout) {
        final TtyEditText editText;
        final SpannableString s;

        if (++mPromptIndex >= mPrompts.length) {
            mPromptIndex = 0;
        }

        editText = createTtyEditText(mAllowEditing, mPrompts[mPromptIndex].length());
        s = styleText(mPrompts[mPromptIndex], new StyleSpan(Typeface.BOLD),
                new ForegroundColorSpan(Color.RED));
        editText.setText(s);
        layout.addView(editText);

        editText.setSingleLine(mSingleLine);
        if (!mSingleLine) { // multi-line gets a "done" key.
            editText.setImeOptions(EditorInfo.IME_ACTION_DONE);
        }

        editText.addTextChangedListener(MainActivity.this);

        editText.requestFocus();

        InputMethodManager imm =
                (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);

        return editText;
    }

    // Echo the user's input to the bottom of the layout.
    private void echoResponse(LinearLayout layout, String enteredText) {
        final TextView textView = new TextView(this);
        final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams
                (LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        SpannableString prefix;
        SpannableString input;

        prefix = styleText("You entered: ", new StyleSpan(Typeface.NORMAL),
                new ForegroundColorSpan(Color.BLACK));
        input = styleText(enteredText, new StyleSpan(Typeface.BOLD), new ForegroundColorSpan(Color.BLACK));
        textView.setLayoutParams(params);
        textView.setTextSize(DEFAULT_TEXT_SIZE);
        textView.setText(TextUtils.concat(prefix, input));
        layout.addView(textView);
    }

    // Invoked from our EditText subclass when the enter key is seen on single line EditText and
    // from this Activity on multiline EditText.
    @Override
    public void onEnter(TtyEditText editText) {
        final String s = editText.getText().toString();
        final String entry = s.substring(editText.getPromptLength(), s.length());

        editText.removeTextChangedListener(this);
        editText.setFocusable(false);
        editText.setFocusableInTouchMode(false);
        editText.setEnabled(false);
        echoResponse(mLinearLayout, entry);
        mCurrentField = generatePrompt(mLinearLayout);
    }

    @Override
    public void afterTextChanged(Editable s) {
        // Do nothing
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        if (mAllowEditing) {
            if (start < mPrompts[mPromptIndex].length()) {
                mCurrentField.removeTextChangedListener(this); // prevent infinite looping
                mCurrentField.setText(s);
                mCurrentField.addTextChangedListener(this);
            }
        } else if (after < count) { // Trying to delete, so don't allow it.
            mCurrentField.removeTextChangedListener(this); // prevent infinite looping
            mCurrentField.setText(s);
            mCurrentField.addTextChangedListener(this);
        }
    }

    // This method only does anything if the input field is multi-line.
    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        if (mSingleLine) {
            return;
        }
        // Multi-line fields get a newline character inserted. Look for the newline, strip it
        // and that will complete field entry.
        int newlinePos = TextUtils.indexOf(s, "\n");
        if (newlinePos != -1) {
            mCurrentField.removeTextChangedListener(this); // prevent infinite looping
            s = TextUtils.concat(s.subSequence(0, newlinePos),
                    s.subSequence(newlinePos + 1, s.length()));
            mCurrentField.setText(s);
            onEnter(mCurrentField);
        }
    }

    // Add text to a TextView (could be an EditText) and apply spans for styling.
    private SpannableString styleText(String text, StyleSpan style,
                                      ForegroundColorSpan foregroundColor) {
        final SpannableString s = new SpannableString(text);

        s.setSpan(style, 0, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        s.setSpan(foregroundColor, 0, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        s.setSpan(new ForegroundColorSpan(Color.GREEN), s.length(), s.length(),
                Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        s.setSpan(new StyleSpan(Typeface.ITALIC), s.length(), s.length(),
                Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        return s;
    }

    // Create an empty LinearLayout within ScrollView.
    private ScrollView createViewGroup() {
        final ScrollView scroll = new ScrollView(this);
        final LinearLayout linearLayout = new LinearLayout(this);
        final FrameLayout.LayoutParams params =
                new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                        FrameLayout.LayoutParams.WRAP_CONTENT);
        final int margins = convertDpToPixels(16f, this);

        params.setMargins(margins, margins, margins, margins);
        scroll.setLayoutParams(params);

        linearLayout.setLayoutParams(new FrameLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT));
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        /* Bottom padding is set here due to a very strange interaction between the
           EditText, the soft keyboard, and the ScrollView. With zero padding, when
           input field abuts the top of the soft keyboard and the user anter a space,
           things get a little wacky with how things are handled. User input freezes and
           there are bad calls made to this app. The bottom padding fixes it, though.
        */
        linearLayout.setPadding(0, 0, 0, convertDpToPixels(16f, this));

        scroll.addView(linearLayout);

        return scroll;
    }

    // Create an instance of our subclass of EditText.
    private TtyEditText createTtyEditText(final boolean allowEditing, final int promptLength) {
        final TtyEditText editText;
        final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams
                (LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);

        editText = new TtyEditText(this);
        editText.setEditParams(allowEditing, promptLength);
        editText.setLayoutParams(params);
        editText.setTextSize(DEFAULT_TEXT_SIZE);
        editText.setBackgroundResource(android.R.color.transparent);
        editText.setInputType(InputType.TYPE_CLASS_TEXT
                | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
                | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
        editText.setPadding(0, 0, 0, 0);
        editText.setFocusable(true);
        editText.setFocusableInTouchMode(true);

        return editText;
    }

    private static int convertDpToPixels(float dp, Context context) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                context.getResources().getDisplayMetrics());
    }

    private static final float DEFAULT_TEXT_SIZE = 18f;
}

<强> TtyEditText.java

public class TtyEditText extends AppCompatEditText {
    boolean mAllowEditing = true;
    int mPromptLength = 0;

    // Interface to activity for the enter key. This can be expanded to accommodate other keys.
    FieldAction mFieldAction;

    public TtyEditText(Context context) {
        this(context, null);
    }

    public TtyEditText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TtyEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mFieldAction = (FieldAction) context;
    }

    // Handle the "enter" and "delete" keys from hardware keyboards.
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {

            case KeyEvent.KEYCODE_ENTER:
                mFieldAction.onEnter(this);
                return true;
        }
        super.onKeyDown(keyCode, event);
        return false;
    }

    // Don't let the user move around in the field. If they try, put them back to the end.
    @Override
    protected void onSelectionChanged(int selStart, int selEnd) {
        //on selection move cursor to end of text
        if (!mAllowEditing) { // No backspace/no cursor movement except to add to end
            setSelection(this.length());
        } else if (selStart < mPromptLength && this.getText().length() > 0) { // can't move into prompt area
            setSelection(mPromptLength);
        } else {
            super.onSelectionChanged(selStart, selEnd);
        }
    }

    public void setEditParams(boolean allowEditing, int promptLength) {
        mAllowEditing = allowEditing;
        mPromptLength = promptLength;
    }

    public int getPromptLength() {
        return mPromptLength;
    }

    interface FieldAction {
        public void onEnter(TtyEditText ttyEditText);
    }
}