Android开源:手把手教你做一款含一键删除&自定义样式的SuperEditText


前言

  • Android开发中,EditText的使用 非常常见
  • 本文将手把手教你做一款 附带一键删除功能 & 自定义样式丰富SuperEditText控件,希望你们会喜欢。
效果图

已在Github开源:Super_EditText,欢迎 Star


目录

示意图

1. 简介

一款 附带一键删除功能 & 自定义样式丰富SuperEditText控件

已在Github开源:Super_EditText,欢迎 Star

效果图

2. 功能介绍

2.1 需求场景

对于 EditText来说,一般的需求有:

  • 方便用户因出现输入错误而进行2次输入
  • 标识用户正在填写项
  • 根据具体场景增加一定的UI元素

2.2 功能需求

根据需求场景,得出EditText需要具备的功能如下:

  • 一键删除
  • 丰富的自定义样式:左侧图标、删除功能图标、分割线 & 光标 样式变化。具体如下图:
示意图

注:该样式的设置是系统自带的 API 所不具备的

  • 功能列表
示意图

2.3 功能示意

效果图

3. 特点

对比市面上EditText控件,该控件Super_EditText 的特点是:

3.1 功能实用

  • 一键删除功能 在需求中非常常见,现将其封装后更加方便使用
  • 可自定义样式程度高(比自带的强大 & 方便),不复杂却能满足一般的EditText使用需求

可自定义样式如下:(注:该样式的设置是系统自带的 API 所不具备的)

示意图

3.2 使用简单

3.3 二次开发成本低

  • 本项目已在 Github上开源:Super_EditText
  • 具备详细的源码分析文档(即本文)

所以,在其上做二次开发 & 定制化成本非常低。


4. 功能详细设计

下面将给出详细的功能逻辑

4.1 一键清空输入字段

  • 描述:将当前用户输入的字段清空
  • 需求场景:方便用户因出现输入错误而进行2次输入
  • 原型图
示意图
  • 源码分析
   /*
    * 步骤1:定义属性
    * */

    private int  ic_deleteResID; // 删除图标 资源ID
    private Drawable  ic_delete; // 删除图标
    private int delete_x,delete_y,delete_width,delete_height; // 删除图标起点(x,y)、删除图标宽、高(px)

    /*
    * 步骤2:初始化属性
    * */
   private void init(Context context, AttributeSet attrs) {

        // 获取控件资源
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SuperEditText);
       /**
         * 初始化删除图标
         */
         // 1. 获取资源ID
         ic_deleteResID = typedArray.getResourceId(R.styleable.SuperEditText_ic_delete,R.drawable.delete);
         // 2. 根据资源ID获取图标资源(转化成Drawable对象)
         ic_delete =  getResources().getDrawable(ic_deleteResID);
         // 3. 设置图标大小
         // 起点(x,y)、宽= left_width、高 = left_height
         delete_x = typedArray.getInteger(R.styleable.SuperEditText_delete_x, 0);
         delete_y = typedArray.getInteger(R.styleable.SuperEditText_delete_y, 0);
         delete_width = typedArray.getInteger(R.styleable.SuperEditText_delete_width, 60);
         delete_height = typedArray.getInteger(R.styleable.SuperEditText_delete_height, 60);
         ic_delete.setBounds(delete_x, delete_y, delete_width, delete_height);

   /**
     * 步骤3:通过监听复写EditText本身的方法来确定是否显示删除图标
     * 监听方法:onTextChanged() & onFocusChanged()
     * 调用时刻:当输入框内容变化时 & 焦点发生变化时
     */
    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        setDeleteIconVisible(hasFocus() && text.length() > 0,hasFocus());
        // hasFocus()返回是否获得EditTEXT的焦点,即是否选中
        // setDeleteIconVisible() = 根据传入的是否选中 & 是否有输入来判断是否显示删除图标->>关注1
    }

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
        setDeleteIconVisible(focused && length() > 0,focused);
        // focused = 是否获得焦点
        // 同样根据setDeleteIconVisible()判断是否要显示删除图标->>关注1
    }

   /**
     * 关注1
     * 作用:判断是否显示删除图标
     */
    private void setDeleteIconVisible(boolean deleteVisible,boolean leftVisible) {
        setCompoundDrawables(leftVisible ?  ic_left_click :  ic_left_unclick, null,
                deleteVisible ?  ic_delete: null, null);

    // setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom)介绍
        // 作用:在EditText上、下、左、右设置图标(相当于android:drawableLeft=""  android:drawableRight="")
        // 备注:传入的Drawable对象必须已经setBounds(x,y,width,height),即必须设置过初始位置、宽和高等信息
        // x:组件在容器X轴上的起点 y:组件在容器Y轴上的起点 width:组件的长度 height:组件的高度
        // 若不想在某个地方显示,则设置为null

        // 另外一个相似的方法:setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, Drawable right, Drawable bottom)
        // 作用:在EditText上、下、左、右设置图标
        // 与setCompoundDrawables的区别:setCompoundDrawablesWithIntrinsicBounds()传入的Drawable的宽高=固有宽高(自动通过getIntrinsicWidth()& getIntrinsicHeight()获取)
        // 不需要设置setBounds(x,y,width,height)
    }


   /**
     * 步骤4:对删除图标区域设置点击事件,即"点击 = 清空搜索框内容"
     * 原理:当手指抬起的位置在删除图标的区域,即视为点击了删除图标 = 清空搜索框内容
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        // 原理:当手指抬起的位置在删除图标的区域,即视为点击了删除图标 = 清空搜索框内容
        switch (event.getAction()) {
            // 判断动作 = 手指抬起时
            case MotionEvent.ACTION_UP:
                Drawable drawable =  ic_delete;

                if (drawable != null && event.getX() <= (getWidth() - getPaddingRight())
                        && event.getX() >= (getWidth() - getPaddingRight() - drawable.getBounds().width())) {

                    // 判断条件说明
                    // event.getX() :抬起时的位置坐标
                    // getWidth():控件的宽度
                    // getPaddingRight():删除图标图标右边缘至EditText控件右边缘的距离
                    // 即:getWidth() - getPaddingRight() = 删除图标的右边缘坐标 = X1
                        // getWidth() - getPaddingRight() - drawable.getBounds().width() = 删除图标左边缘的坐标 = X2
                    // 所以X1与X2之间的区域 = 删除图标的区域
                    // 当手指抬起的位置在删除图标的区域(X2=<event.getX() <=X1),即视为点击了删除图标 = 清空搜索框内容
                    setText("");

                }
                break;
        }
        return super.onTouchEvent(event);
    }

示意图

4.2 选中样式

  • 描述:通过增加UI元素 & 交互样式表示用户正在填写的项目
  • 需求场景:标识用户正在填写项
  • 样式说明
示意图
  • 原型图
示意图
  • 属性说明
示意图
示意图
  • 源码分析
   /*
    * 步骤1:定义属性
    * */
    private Paint mPaint; // 画笔
    private int  ic_left_clickResID,ic_left_unclickResID;    // 左侧图标 资源ID(点击 & 无点击)
    private Drawable  ic_left_click,ic_left_unclick; // 左侧图标(点击 & 未点击)
    private int left_x,left_y,left_width,left_height; // 左侧图标起点(x,y)、左侧图标宽、高(px)

    private int cursor; // 光标

    // 分割线变量
    private int lineColor_click,lineColor_unclick;// 点击时 & 未点击颜色
    private int color; 
    private int linePosition; // 分割线位置

   /*
    * 步骤2:初始化属性
    * */
private void init(Context context, AttributeSet attrs) {

        // 获取控件资源
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SuperEditText);

        /**
         * 初始化左侧图标(点击 & 未点击)
         */

        // a. 点击状态的左侧图标
         // 1. 获取资源ID
         ic_left_clickResID = typedArray.getResourceId(R.styleable.SuperEditText_ic_left_click, R.drawable.ic_left_click);
         // 2. 根据资源ID获取图标资源(转化成Drawable对象)
         ic_left_click =  getResources().getDrawable(ic_left_clickResID);
         // 3. 设置图标大小
         // 起点(x,y)、宽= left_width、高 = left_height
         left_x = typedArray.getInteger(R.styleable.SuperEditText_left_x, 0);
         left_y = typedArray.getInteger(R.styleable.SuperEditText_left_y, 0);
         left_width = typedArray.getInteger(R.styleable.SuperEditText_left_width, 60);
         left_height = typedArray.getInteger(R.styleable.SuperEditText_left_height, 60);

         ic_left_click.setBounds(left_x, left_y,left_width, left_height);
         // Drawable.setBounds(x,y,width,height) = 设置Drawable的初始位置、宽和高等信息
         // x = 组件在容器X轴上的起点、y = 组件在容器Y轴上的起点、width=组件的长度、height = 组件的高度

        // b. 未点击状态的左侧图标
         // 1. 获取资源ID
         ic_left_unclickResID = typedArray.getResourceId(R.styleable.SuperEditText_ic_left_unclick, R.drawable.ic_left_unclick);
         // 2. 根据资源ID获取图标资源(转化成Drawable对象)
         // 3. 设置图标大小(此处默认左侧图标点解 & 未点击状态的大小相同)
         ic_left_unclick =  getResources().getDrawable(ic_left_unclickResID);
         ic_left_unclick.setBounds(left_x, left_y,left_width, left_height);

        /**
         * 设置EditText左侧图片(初始状态仅有左侧图片))
         */
        setCompoundDrawables( ic_left_unclick, null,
                null, null);

        // setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom)介绍
        // 作用:在EditText上、下、左、右设置图标(相当于android:drawableLeft=""  android:drawableRight="")
        // 备注:传入的Drawable对象必须已经setBounds(x,y,width,height),即必须设置过初始位置、宽和高等信息
        // x:组件在容器X轴上的起点 y:组件在容器Y轴上的起点 width:组件的长度 height:组件的高度
        // 若不想在某个地方显示,则设置为null

        // 另外一个相似的方法:setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, Drawable right, Drawable bottom)
        // 作用:在EditText上、下、左、右设置图标
        // 与setCompoundDrawables的区别:setCompoundDrawablesWithIntrinsicBounds()传入的Drawable的宽高=固有宽高(自动通过getIntrinsicWidth()& getIntrinsicHeight()获取)
        // 不需要设置setBounds(x,y,width,height)

        /**
         * 初始化光标(颜色 & 粗细)
         */
         // 原理:通过 反射机制 动态设置光标
         // 1. 获取资源ID
         cursor = typedArray.getResourceId(R.styleable.SuperEditText_cursor, R.drawable.cursor);
         try {

            // 2. 通过反射 获取光标属性
            Field f = TextView.class.getDeclaredField("mCursorDrawableRes");
            f.setAccessible(true);
            // 3. 传入资源ID
            f.set(this, cursor);

         } catch (Exception e) {
            e.printStackTrace();
         }

        /**
         * 初始化分割线(颜色、粗细、位置)
         */
         // 1. 设置画笔
         mPaint = new Paint();
         mPaint.setStrokeWidth(2.0f); // 分割线粗细

         // 2. 设置分割线颜色(使用十六进制代码,如#333、#8e8e8e)
         int lineColorClick_default = context.getResources().getColor(R.color.lineColor_click); // 默认 = 蓝色#1296db
         int lineColorunClick_default = context.getResources().getColor(R.color.lineColor_unclick); // 默认 = 灰色#9b9b9b
         lineColor_click = typedArray.getColor(R.styleable.SuperEditText_lineColor_click, lineColorClick_default);
         lineColor_unclick = typedArray.getColor(R.styleable.SuperEditText_lineColor_unclick, lineColorunClick_default);
         color = lineColor_unclick;

         mPaint.setColor(lineColor_unclick); // 分割线默认颜色 = 灰色
         setTextColor(color); // 字体默认颜色 = 灰色

         // 3. 分割线位置
         linePosition = typedArray.getInteger(R.styleable.SuperEditText_linePosition, 5);
         // 消除自带下划线
         setBackground(null);


   /**
     * 步骤3:通过监听复写EditText本身的方法来设置所有样式
     * 监听方法:onTextChanged() & onFocusChanged()
     * 调用时刻:当输入框内容变化时 & 焦点发生变化时
     */
      @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        setDeleteIconVisible(hasFocus() && text.length() > 0,hasFocus());
        // hasFocus()返回是否获得EditTEXT的焦点,即是否选中
        // setDeleteIconVisible() = 根据传入的是否选中 & 是否有输入来判断是否显示删除图标->>关注1
    }

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
        setDeleteIconVisible(focused && length() > 0,focused);
        // focused = 是否获得焦点
        // 同样根据setDeleteIconVisible()判断是否要显示删除图标->>关注1
    }

    /**
     * 关注1
     * 作用:设置分割线颜色
     */
    private void setDeleteIconVisible(boolean deleteVisible,boolean leftVisible) {
        color = leftVisible ? lineColor_click : lineColor_unclick;
        setTextColor(color);
        invalidate();
    }

   /**
     * 步骤4:绘制分割线
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(color);
        setTextColor(color);
        // 绘制分割线
        // 需要考虑:当输入长度超过输入框时,所画的线需要跟随着延伸
        // 解决方案:线的长度 = 控件长度 + 延伸后的长度
        int x=this.getScrollX(); // 获取延伸后的长度
        int w=this.getMeasuredWidth(); // 获取控件长度

        // 传入参数时,线的长度 = 控件长度 + 延伸后的长度
                canvas.drawLine(0, this.getMeasuredHeight()- linePosition, w+x,
                        this.getMeasuredHeight() - linePosition, mPaint);

    }
}

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SuperEditText">

        <attr name="ic_delete" format="reference" />
        <attr name="delete_x" format="integer"  />
        <attr name="delete_y" format="integer"  />
        <attr name="delete_width" format="integer"  />
        <attr name="delete_height" format="integer"  />
        
        <attr name="ic_left_click" format="reference" />
        <attr name="ic_left_unclick" format="reference" />
        <attr name="left_x" format="integer"  />
        <attr name="left_y" format="integer"  />
        <attr name="left_width" format="integer"  />
        <attr name="left_height" format="integer"  />

        <attr name="lineColor_click" format="color"  />
        <attr name="lineColor_unclick" format="color"  />
        <attr name="linePosition" format="integer"  />

        <attr name="cursor" format="reference" />

    </declare-styleable>
</resources>

cursor.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <solid android:color="@color/lineColor_click" />
    <size android:width="1dp" />

</shape>

5. 完整源码地址

Carson_Ho的Github地址:Super_EditText


6. 具体使用

具体请看文章:Android自定义View:你需要一款简单实用的SuperEditText(一键删除&自定义样式)


7. 贡献代码

  • 希望你们能和我一起完善这款简单 & 好用的SuperEditText控件,具体请看:贡献代码说明
  • 关于该开源项目的意见 & 建议可在Issue上提出。欢迎 Star

8. 总结

相信你一定会喜欢上 这款简单 & 好用的SuperEditText控件

已在Github上开源:Super_EditText,欢迎 Star

效果图

Carson带你学自定义View文章系列:
Carson带你学自定义View:自定义View基础
Carson带你学自定义View:一文梳理自定义View工作流程
Carson带你学自定义View:Measure过程
Carson带你学自定义View:Layout过程
Carson带你学自定义View:Draw过程
Carson带你学自定义View:手把手教你写一个完整的自定义View
Carson带你学自定义View:Canvas类全面解析
Carson带你学自定义View:Path类全面解析


欢迎关注Carson_Ho的简书

不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度


请点赞!因为你的鼓励是我写作的最大动力!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,585评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,923评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,314评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,346评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,718评论 3 291
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,828评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,020评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,758评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,486评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,722评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,196评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,546评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,211评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,132评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,916评论 0 200
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,904评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,758评论 2 274

推荐阅读更多精彩内容