在 EditText 中放置不可编辑的常量文本-Android

我希望编辑器中有常量文本,比如:

http://<here_user_can_write>

用户不应该能够删除任何字符从“ http://”,我搜索了这一点,发现这:

editText.setFilters(new InputFilter[] {
new InputFilter() {
public CharSequence filter(CharSequence src, int start,
int end, Spanned dst, int dstart, int dend) {
return src.length() < 1 ? dst.subSequence(dstart, dend) : "";
}
}
});

但我不知道它是否限制用户不删除任何字符从 从头到尾限制。我也不能理解限制类的使用。

如果我们可以在 EditText里面放一个 TextView的话,有一种方法会是一个很好的选择,但是我不认为这在 Android 中是可能的,因为两者都是视图,这可能吗?

83940 次浏览

Did u try this method?

final EditText edt = (EditText) findViewById(R.id.editText1);


edt.setText("http://");
Selection.setSelection(edt.getText(), edt.getText().length());




edt.addTextChangedListener(new TextWatcher() {


@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// TODO Auto-generated method stub


}


@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// TODO Auto-generated method stub


}


@Override
public void afterTextChanged(Editable s) {
if(!s.toString().startsWith("http://")){
edt.setText("http://");
Selection.setSelection(edt.getText(), edt.getText().length());


}


}
});

There was a slight problem with @Rajitha Siriwardena's answer. It assumes that the entire string except the suffix has been deleted before the suffix is meaning if you have the string

http://stackoverflow.com/

and try to delete any part of http:// you will delete stackoverflow.com/ resulting in only http://.

I also added a check incase the user tries to input before the prefix.

 @Override
public void afterTextChanged(Editable s) {
String prefix = "http://";
if (!s.toString().startsWith(prefix)) {
String cleanString;
String deletedPrefix = prefix.substring(0, prefix.length() - 1);
if (s.toString().startsWith(deletedPrefix)) {
cleanString = s.toString().replaceAll(deletedPrefix, "");
} else {
cleanString = s.toString().replaceAll(prefix, "");
}
editText.setText(prefix + cleanString);
editText.setSelection(prefix.length());
}
}

Note: this doesn't handle the case where the user tries to edit the prefix itself only before and after.

Here is a less efficient solution that should handle all cases for when characters OR words are deleted/inserted in OR around the prefix.

prefix = "http://"
extra = "ahhttp://"
differencePrefix(prefix, extra) = "aht"

Code:

public static String differencePrefix(String prefix, String extra) {
if (extra.length() < prefix.length()) return "";
StringBuilder sb = new StringBuilder();
StringBuilder eb = new StringBuilder();
int p = 0;
for (int i = 0; i < extra.length(); i++) {
if (i >= prefix.length()) {
while(p < extra.length()) {
eb.append(extra.charAt(p));
p++;
}
break;
}
if (p >= extra.length()) break;
char pchar = extra.charAt(p);
char ichar = prefix.charAt(i);
while(pchar != ichar) {
//check if char was deleted
int c = i + 1;
if (c < prefix.length()) {
char cchar = prefix.charAt(c);
if (cchar == pchar) {
break;
}
}
sb.append(pchar);
p++;
if (p >= extra.length()) break;
pchar = extra.charAt(p);
}
p++;
}


return eb.toString() + sb.toString();
}

You can use it like this

editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {


}


@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {


}


@Override
public void afterTextChanged(Editable s) {
String input = s.toString();
if (!input.startsWith(prefix)) {
String extra = differencePrefix(prefix, input);
String newInput = prefix + extra;
editText.setText(newInput);
editText.setSelection(newInput.length());
}
}
});

You had it almost right, try

private final String PREFIX="http://";


editText.setFilters(new InputFilter[]{new InputFilter() {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int
dend) {
return dstart<PREFIX.length()?dest.subSequence(dstart,dend):null;
}
}});

That's how you can actually do it with an InputFilter:

final String prefix = "http://"
editText.setText(prefix);


editText.setFilters(new InputFilter[] {
new InputFilter() {
@Override
public CharSequence filter(final CharSequence source, final int start,
final int end, final Spanned dest, final int dstart, final int dend) {
final int newStart = Math.max(prefix.length(), dstart);
final int newEnd = Math.max(prefix.length(), dend);
if (newStart != dstart || newEnd != dend) {
final SpannableStringBuilder builder = new SpannableStringBuilder(dest);
builder.replace(newStart, newEnd, source);
if (source instanceof Spanned) {
TextUtils.copySpansFrom(
(Spanned) source, 0, source.length(), null, builder, newStart);
}
Selection.setSelection(builder, newStart + source.length());
return builder;
} else {
return null;
}
}
}
});

If you also want the prefix to be not selectable you can add the following code.

final SpanWatcher watcher = new SpanWatcher() {
@Override
public void onSpanAdded(final Spannable text, final Object what,
final int start, final int end) {
// Nothing here.
}


@Override
public void onSpanRemoved(final Spannable text, final Object what,
final int start, final int end) {
// Nothing here.
}


@Override
public void onSpanChanged(final Spannable text, final Object what,
final int ostart, final int oend, final int nstart, final int nend) {
if (what == Selection.SELECTION_START) {
if (nstart < prefix.length()) {
final int end = Math.max(prefix.length(), Selection.getSelectionEnd(text));
Selection.setSelection(text, prefix.length(), end);
}
} else if (what == Selection.SELECTION_END) {
final int start = Math.max(prefix.length(), Selection.getSelectionEnd(text));
final int end = Math.max(start, nstart);
if (end != nstart) {
Selection.setSelection(text, start, end);
}
}
}
};


editText.getText().setSpan(watcher, 0, 0, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

I know I'm reviving an old post but I want to share with the community that I have struggled with this topic these days and I found that placing a TextView over the EditText is not only perfectly doable (to respond to the second part of the question), much more in this case when the constant text is needed in the starting position, but preferable, too. Moreover the cursor won't even move before the "mutable" text at all, which is an elegant effect. I prefer this solution because it doesn't add workload and complexity to my app with listeners and whatsoever.

Here's a sample code of my solution:

<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="3dp"
android:text="http://" />


<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textUri"
android:paddingStart="choose an appropriate padding" />
</RelativeLayout>

By enclosing the views in a RelativeLayout they will be overlapped. The trick here is playing with the android:paddingStart property of the EditText, to make the text start just right after the TextView, while android:layout_centerVertical="true" and android:layout_marginStart="3dp" properties of the TextView make sure that its text is correctly aligned with text inputted and with the start of the underlying line of the EditText (or at least this happens when using a Material themed one).

Taken from Ali Muzaffar's blog, see the original post for more details.

Use custom EditText View to draw the prefix text and add padding according to the prefix text size:

public class PrefixEditText extends EditText {


private String mPrefix = "$"; // add your prefix here for example $
private Rect mPrefixRect = new Rect(); // actual prefix size


public PrefixEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
getPaint().getTextBounds(mPrefix, 0, mPrefix.length(), mPrefixRect);
mPrefixRect.right += getPaint().measureText(" "); // add some offset


super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}


@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(mPrefix, super.getCompoundPaddingLeft(), getBaseline(), getPaint());
}


@Override
public int getCompoundPaddingLeft() {
return super.getCompoundPaddingLeft() + mPrefixRect.width();
}
}

CODE TO ADD CUSTOM PREFIX TO YOUR EDITTEXT (PREFIX NOT EDITABLE)

Code from Medium by Ali Muzaffar

public class PrefixEditText extends AppCompatEditText {
float originalLeftPadding = -1;


public PrefixEditText(Context context) {
super(context);
}


public PrefixEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}


public PrefixEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
calculatePrefix();
}


private void calculatePrefix() {
if (originalLeftPadding == -1) {
String prefix = (String) getTag();
float[] widths = new float[prefix.length()];
getPaint().getTextWidths(prefix, widths);
float textWidth = 0;
for (float w : widths) {
textWidth += w;
}
originalLeftPadding = getCompoundPaddingLeft();
setPadding((int) (textWidth + originalLeftPadding),
getPaddingRight(), getPaddingTop(),
getPaddingBottom());
}
}


@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String prefix = (String) getTag();
canvas.drawText(prefix, originalLeftPadding, getLineBounds(0, null), getPaint());
}
}

And XML

<com.yourClassPath.PrefixEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom"
android:textSize="14sp"
android:tag="€ " />

I just found the solution how to make prefix not-editable and how to save text if you try to remove prefix. That's very close to @Rajitha Siriwardena answer. All you missed is to save text before any changes applied. It will be restored in afterTextChanged(...).

Code:

final String prefix = "http://";
editText.setText(prefix);
Selection.setSelection(editText.getText(), editText.getText().length());


editText.addTextChangedListener(new TextWatcher() {
String text;
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
text = charSequence.toString();
}


@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}


@Override
public void afterTextChanged(Editable editable) {
if (!editable.toString().startsWith(prefix)) {
editText.setText(text);
Selection.setSelection(editText.getText(), editText.getText().length());
}
}
});
    EditText msg=new EditText(getContext());
msg.setSingleLine(true);
msg.setSingleLine();
msg.setId(View.generateViewId());
msg.measure(0,0);






TextView count=new TextView(getContext());
count.setTextColor(Color.parseColor("#666666"));
count.setText("20");
count.setPadding(0,0,(int)Abstract.getDIP(getContext(),10),0);
count.measure(0,0);
float tenPIX =TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,getContext().getResources().getDisplayMetrics());


msg.setPadding((int)tenPIX,(int)tenPIX,(int)(int)tenPIX+count.getMeasuredWidth(),(int)tenPIX);




RelativeLayout ll1=new RelativeLayout(getContext());
ll1.addView(msg,new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));


LayoutParams countlp=new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
countlp.addRule(RelativeLayout.ALIGN_END,msg.getId());
countlp.addRule(RelativeLayout.ALIGN_BASELINE,msg.getId());
ll1.addView(count,countlp);

This one is basically to add prefix "+91" to your edit text field of phone number.

1.Add this code to oncreate() of activity

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sign_up);


// Write other things......//


etPhoneNumber.setFilters(new InputFilter[]{getPhoneFilter(),newInputFilter.LengthFilter(13)});


etPhoneNumber.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
if (etPhoneNumber.getText().toString().isEmpty()) {
etPhoneNumber.setText("+91");
Selection.setSelection(etPhoneNumber.getText(), etPhoneNumber.getText().length());                    }
} else {
if (etPhoneNumber.getText().toString().equalsIgnoreCase("+91")) {
etPhoneNumber.setFilters(new InputFilter[]{});
etPhoneNumber.setText("");
etPhoneNumber.setFilters(new InputFilter[]{getPhoneFilter(),new InputFilter.LengthFilter(13)});


}
}
}
});
}

2.Declare a method called getPhoneFilter()

    private InputFilter getPhoneFilter() {


Selection.setSelection(etPhoneNumber.getText(), etPhoneNumber.getText().length());


etPhoneNumber.addTextChangedListener(new TextWatcher() {


@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}


@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}


@Override
public void afterTextChanged(Editable s) {
if(!s.toString().startsWith("+91")){
if (etPhoneNumber.getFilters() != null && etPhoneNumber.getFilters().length > 0) {
etPhoneNumber.setText("+91");
Selection.setSelection(etPhoneNumber.getText(), etPhoneNumber.getText().length());
}
}
}
});


// Input filter to restrict user to enter only digits..
InputFilter filter = new InputFilter() {


public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {


for (int i = start; i < end; i++) {


if (!String.valueOf(getString(R.string.digits_number)).contains(String.valueOf(source.charAt(i)))) {
return "";
}
}
return null;
}
};
return filter;
}

3.declare "digits_number" in your values file

    <string name="digits_number">1234567890+</string>

The code below works for me. It handles cases when the user edits the prefix, deletes it, inserts text from the buffer, changes the selected text. If the user changes the prefix, the focus moves to the end of the prefix.

    final String prefix = "http://";
final String[] aLastText = {prefix};
et.setText(prefix);
et.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void afterTextChanged(Editable sNew) {


if (!sNew.toString().startsWith(prefix)) {
String sLast = aLastText[0];
boolean isRemoving = sNew.length() < sLast.length();


int start;
int end = sNew.length() - 1;
for (start = 0; start < sLast.length() && start < sNew.length(); start++) {
if (sLast.charAt(start) != sNew.charAt(start))
break;
}
int k = sLast.length() - 1;
for (; end >= start && k >= 0; end--, k--) {
if (sLast.charAt(k) != sNew.charAt(end))
break;
}
String sEdited = sNew.toString().substring(start, ++end);
k += isRemoving ? 1 : 0;
k = k < prefix.length() ? prefix.length() : k;
String sSuffix = sLast.substring(k, sLast.length());


et.setText(prefix + sEdited + sSuffix);
et.setSelection(et.getText().length() - sSuffix.length());
}
aLastText[0] = et.getText().toString();
}
});

Examples:

ht5tp://localhost, 5http://localhost, http:/5/localhost -> http://5localhost

http:localhost -> http://localhost

Based on @demaksee comment. I extend EditText and override function onSelectionChanged. So user even can`t edit prefix. Very simple and useful. Kotlin:

private var prefix : String? = ""


override fun onSelectionChanged(selStart: Int, selEnd: Int) {
if (prefix != null && prefix!!.isNotBlank()) {
var finalStart = selStart
var finalEnd = selEnd


val prefixLength = prefix!!.length
if (prefixLength > selStart) {
finalStart = prefixLength
}


if (prefixLength > selEnd) {
finalEnd = prefixLength
}


if (finalStart == selStart && finalEnd == selEnd) {
super.onSelectionChanged(finalStart, finalEnd)
} else {
val startWithPrefix = text?.startsWith(prefix ?: "") ?: prefix.isNullOrBlank()
if (!startWithPrefix) {
setText(prefix)
}


setSelection(finalStart, finalEnd)
}
}
}


public fun setPrefix(prefix: String) {
editText.setText(prefix)
editText.setSelection(prefix.length)
this.prefix = prefix
}

As of version 1.2.0-alpha01 of material design library, prefix and suffix is supported for text fields:

<com.google.android.material.textfield.TextInputLayout
app:prefixText="Price: "
app:prefixTextAppearance="..."
app:prefixTextColor="..."
app:suffixText="Dollar"
app:suffixTextColor="..."
app:suffixTextAppearance="...">


<com.google.android.material.textfield.TextInputEditText .../>


</com.google.android.material.textfield.TextInputLayout>

The only downside in my opinion is that the suffix is fixed at the end of the text field and there is no option to make it flow with the input text. You can vote on this issue for that.

I made Kotlin extension function for adding prefix to EditText

fun EditText.addPrefix(prefix: String) {
var text = ""
var isPrefixModified = false
val formattedPrefix = "$prefix "
var lastCharSequence: CharSequence? = null


val setEditText: () -> Unit = {
setText(text)
Selection.setSelection(editableText, text.length)
}


this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
val newText = editable.toString()


when {
isPrefixModified -> {
isPrefixModified = false
setEditText()
}
isTryingToDeletePrefix(newText) -> {
setEditText()
}
isNewInput(newText) -> {
text = "$formattedPrefix$newText"
setEditText()
}
else -> {
text = newText
}
}
}


override fun beforeTextChanged(charSequence: CharSequence?, start: Int,
count: Int, after: Int) {
charSequence?.let {
if (it != lastCharSequence && it.isNotEmpty() && start <= prefix.length) {
isPrefixModified = true
}
lastCharSequence = charSequence
}
}


override fun onTextChanged(charSequence: CharSequence?, start: Int,
before: Int, count: Int) {
// Ignore
}


private fun isTryingToDeletePrefix(newText: String) =
text.isNotEmpty() && newText.length < text.length && isNewInput(newText)


private fun isNewInput(newText: String) = !newText.contains(formattedPrefix)
})
}

An easy to use Kotlin extension function for this purpose

fun EditText.stickPrefix(prefix: String) {
this.addTextChangedListener(afterTextChanged = {
if (!it.toString().startsWith(prefix) && it?.isNotEmpty() == true) {
this.setText(prefix + this.text)
this.setSelection(this.length())


}
})
}
//someEditText.stickPrefix("+")

what worked for me is to add some changes on Rajitha Siriwardena code :

First, put text on the Edittext or TextInputEditText xml layout :
android:text="http://" the purpose is to test the if condition on the first attempt

Second, test the condition with if like this

@Override
public void afterTextChanged(Editable s) {
if (!s.toString().startsWith("http://")) {
etPhone.setText("http://");
etPhone.setSelection(etPhone.length());
}

I am baffled by the complex answers posted. More easier way will be to add a textview with code as text as a prefix and put some elevation. This way, you will much finer control over the designing of the code("+91"). Here's an example of the same.

<androidx.constraintlayout.widget.ConstraintLayout


android:id="@+id/mobile_bottom_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/card_with_top_radius"
android:elevation="12dp"
android:paddingBottom="@dimen/dim_18"
app:flow_verticalAlign="bottom"
app:layout_constraintBottom_toBottomOf="parent">




  



<TextView
android:id="@+id/code"
style="@style/color_333333_text_14_roboto_regular_venus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dim_16"
android:text="+91"
android:elevation="1dp"
app:layout_constraintBottom_toBottomOf="@id/number"
app:layout_constraintStart_toStartOf="@id/number"
app:layout_constraintTop_toTopOf="@id/number" />




<com.gradeup.baseM.view.custom.TabletEditText
android:id="@+id/number"
style="@style/color_333333_text_12_roboto_medium_venus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/dim_16"
android:layout_marginTop="@dimen/dim_17"
android:layout_marginBottom="@dimen/dim_16"
android:background="@drawable/e6e6e6_4dp_curved_border_white_bg"
android:hint="@string/enter_mobile_number"
android:imeOptions="flagNoExtractUi"
android:inputType="number"
android:maxLength="10"
android:maxLines="1"
android:paddingVertical="@dimen/dim_17"
android:paddingStart="@dimen/dim_50"
android:paddingEnd="20dp"
android:textColorHint="@color/color_999999_8799ae"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subTitle">






</com.gradeup.baseM.view.custom.TabletEditText>








</androidx.constraintlayout.widget.ConstraintLayout>

Things to keep in mind :-

  • Adjust editText paddingStart attribute according to your need.
  • Put some elevation in code TextView.