Android Kotlin:不依赖第三方库实现自定义OTP输入视图
自定义实现Android OTP输入视图(无需第三方库/多EditText)
要实现这类OTP输入框,不用第三方库或多个EditText的核心思路是:基于单个EditText自定义View,通过重写onDraw()绘制出多分隔框的视觉效果,同时控制输入长度、字符定位和交互逻辑,既避免了多EditText的状态同步问题,又完全可控。
步骤1:定义自定义属性(可选,方便XML配置)
先在res/values/attrs.xml中定义可配置的属性,比如OTP位数、框大小、间距、颜色等:
<resources> <declare-styleable name="CustomOtpView"> <attr name="otpCount" format="integer" /> <attr name="otpBoxSize" format="dimension" /> <attr name="otpBoxSpacing" format="dimension" /> <attr name="otpBorderColor" format="color" /> <attr name="otpBorderWidth" format="dimension" /> <attr name="otpTextSize" format="dimension" /> <attr name="otpTextColor" format="color" /> </declare-styleable> </resources>
步骤2:实现自定义OTP View
创建继承自AppCompatEditText的自定义View,完成初始化、绘制逻辑和交互处理:
public class CustomOtpView extends AppCompatEditText { private int otpCount = 6; private float boxSize; private float boxSpacing; private int borderColor; private float borderWidth; private Paint boxPaint; private Paint textPaint; private int cursorPosition = 0; public CustomOtpView(Context context) { super(context); init(null); } public CustomOtpView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public CustomOtpView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } private void init(AttributeSet attrs) { // 读取自定义属性 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CustomOtpView); otpCount = ta.getInt(R.styleable.CustomOtpView_otpCount, 6); boxSize = ta.getDimension(R.styleable.CustomOtpView_otpBoxSize, dpToPx(48)); boxSpacing = ta.getDimension(R.styleable.CustomOtpView_otpBoxSpacing, dpToPx(12)); borderColor = ta.getColor(R.styleable.CustomOtpView_otpBorderColor, Color.BLACK); borderWidth = ta.getDimension(R.styleable.CustomOtpView_otpBorderWidth, dpToPx(1)); float textSize = ta.getDimension(R.styleable.CustomOtpView_otpTextSize, spToPx(16)); int textColor = ta.getColor(R.styleable.CustomOtpView_otpTextColor, Color.BLACK); ta.recycle(); // 配置EditText基础行为 setInputType(InputType.TYPE_CLASS_NUMBER); setFilters(new InputFilter[]{new InputFilter.LengthFilter(otpCount)}); setBackgroundColor(Color.TRANSPARENT); setCursorVisible(false); // 隐藏系统光标,自己绘制视觉光标 setPadding(0, 0, 0, 0); // 初始化绘制画笔 boxPaint = new Paint(Paint.ANTI_ALIAS_FLAG); boxPaint.setColor(borderColor); boxPaint.setStyle(Paint.Style.STROKE); boxPaint.setStrokeWidth(borderWidth); textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint.setColor(textColor); textPaint.setTextSize(textSize); textPaint.setTextAlign(Paint.Align.CENTER); // 监听文本变化,更新光标位置 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) { cursorPosition = s.length(); invalidate(); // 触发重绘 } @Override public void afterTextChanged(Editable s) {} }); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 计算整体宽度,居中绘制 float totalWidth = otpCount * boxSize + (otpCount - 1) * boxSpacing; float startX = (getWidth() - totalWidth) / 2; float startY = (getHeight() - boxSize) / 2; // 循环绘制每个OTP框和字符 for (int i = 0; i < otpCount; i++) { float left = startX + i * (boxSize + boxSpacing); float right = left + boxSize; float top = startY; float bottom = top + boxSize; // 高亮当前光标位置的框 if (i == cursorPosition && hasFocus()) { boxPaint.setColor(Color.parseColor("#2196F3")); boxPaint.setStrokeWidth(borderWidth * 2); } else { boxPaint.setColor(borderColor); boxPaint.setStrokeWidth(borderWidth); } canvas.drawRect(left, top, right, bottom, boxPaint); // 绘制对应位置的字符 if (i < getText().length()) { String charStr = String.valueOf(getText().charAt(i)); // 垂直居中计算文本Y坐标 float textY = startY + boxSize / 2 - (textPaint.descent() + textPaint.ascent()) / 2; canvas.drawText(charStr, left + boxSize / 2, textY, textPaint); } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 自定义View的宽高计算 float totalWidth = otpCount * boxSize + (otpCount - 1) * boxSpacing; int width = (int) totalWidth + getPaddingLeft() + getPaddingRight(); int height = (int) boxSize + getPaddingTop() + getPaddingBottom(); setMeasuredDimension(width, height); } // 处理焦点变化,自动弹出键盘 @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); if (focused) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT); } } // 优化删除键行为 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_DEL && getText().length() == 0) { // 文本为空时,拦截删除键,避免光标错位 return true; } return super.onKeyDown(keyCode, event); } // 工具方法:dp转px private float dpToPx(float dp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } // 工具方法:sp转px private float spToPx(float sp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics()); } }
步骤3:在布局中使用自定义View
直接在XML布局中引用,可通过自定义属性配置样式:
<com.yourpackage.name.CustomOtpView android:id="@+id/otpView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" app:otpCount="6" app:otpBoxSize="48dp" app:otpBoxSpacing="12dp" app:otpBorderColor="@color/black" app:otpBorderWidth="1dp" app:otpTextSize="18sp" app:otpTextColor="@color/black" />
可选优化点
- 密码模式:将绘制的字符替换为
●,只需修改onDraw()中绘制文本的逻辑:String charStr = "●"; // 替换原字符 - 错误状态:添加
setErrorState(boolean isError)方法,修改边框颜色并触发重绘:public void setErrorState(boolean isError) { borderColor = isError ? Color.RED : Color.BLACK; invalidate(); } - 横竖屏状态保存:重写
onSaveInstanceState和onRestoreInstanceState,保存光标位置和文本状态(EditText默认会保存文本,自定义状态需额外处理)。
内容的提问来源于stack exchange,提问作者Nikitha
相关产品推荐
相关产品推荐

