Android自定义控件:带动画效果的手机号输入框 (3-4-4格式)

点击上方“Android技术杂货铺”,选择“置顶公众号”

干货文章,第一时间送达!

作者:Android草根王
链接:https://www.jianshu.com/p/e538b35c68c3
本文经作者授权推送。

项目中很多地方,使用到了自定义控件。

简单点的,如个性控件的定制,多个组件的组合封装等。
我们需要了解自定义控件的基础知识,即可快速实现;

复杂点的,如各种图形报表(例如:股票K线图、分时图控件)。
我们除了自定义控件的基础知识,还需要掌握控件事件的拦截传递机制,事件回调、手势识别、画图、 动画、架构设计等技术。

 

关于自定义控件,我们逐步深入讲解:
今天,我们先来实现一个简单的自定义控件,后期找时间再讲解股票K线图、分时图控件如何自定义。

需求

实现带动画效果的手机号输入框:

1.输入手机号格式为3-4-4;
2.输入框中默认有hint提示,当开始输入数字时,有动画效果:
    a) hint平移出输入框,停留在输入框上方指定位置,显示对应的信息;
    b) 平移过程中,文字也逐渐由大变小;
3.当清空输入框,反效果动画;
4.输入时自动做数字格式校验(非数字不让输入)和长度校验(最多11位手机号)
5.当输入框有值后,最右边出现清空按钮,点击清空输入框
6.输入完成,回调结果;

 

分析

该输入框效果在多个页面中都会使用到,我们必须对其进行封装,此处最好的封装方案就是自定义控件。
我们APP中,所有页面的手机号输入框输入逻辑完全一样,但是个别页面存在小差异(个别页面输入手机号时,不需要动画效果,或者hint内容、提示消息不一样等等);

1,差异项应可配置:

自定义控件的以下内容应设计成可以配置的属性:

 是否有动画效果;
   hint文本;
   hint移动到上部显示的文本等;

此处的重点:
1. 自定义属性如何配置?如何使用?

2,自定义控件被调用(使用)

   应支持在代码中直接new 我们的控件
   应支持在布局xml中直接使用我们的控件,可配置自定义属性

3,动画

平移效果:Tween动画、属性动画均可实现;
字体伸缩:应使用属性动画,根据字号去伸缩,宽高也会自动变化(注意:Tween动画无法做字号差值变化)
综上所述,应统一使用属性动画实现平移和伸缩的效果,而多个动画同时触发,会用到动画集合;

此处的重点:
1. 平移需要原始点、目标点两个坐标(x,y),自定义控件中如何获得对应的值?
2. 字体伸缩,需要伸缩前后的两个字号值,代码中默认获得的字号是px格式,如何与sp转换?
3. 设计点:要实现文本移动和字号伸缩的动画效果,我们可以在布局中放置2个文本控件,
tv_message:作为hint占位,不显示,仅用于获得坐标和字号;
tv_to_message: 作为顶部消息显示,作为hint显示,动画执行在该控件上;

当然,如何设计这种动画效果,还有很多其他的方式,大家使用时,可以根据自己的需要,合理设计。

4,手机号的3-4-4格式,就是拦截输入事件,处理字符串,没什么技术点;
5,手机号长度、特殊字符禁止输入验证,可使用正则表达式判断非法字符;
6,其他:略

技术实现分析

属性的定义,需要单独定义在res下的文件中:
res/values目录中,创建attrs.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--用户-手机号输入控件-自定义属性-->
    <declare-styleable name="user.phone.edittext">
        <attr name="showTopMessage" format="boolean"/>
        <attr name="topMessage" format="string"/>
        <attr name="hint" format="string"/>
    </declare-styleable>
</resources>

name可以自定义,规范即可;
showTopMessage //自定义属性:是否显示顶端提示信息(true:显示,false:不显示)
topMessage//自定义属性:顶端提示信息内容
hint //自定义属性:输入框提示信息

2 . 自定义控件的布局

使用相对布局,内容包括:
输入框 et_phone(需设置成无背景色,因UI人员已固定输入线的颜色)、
输入框的底部线 View、
输入框的清空按钮 iv_phone_clear、
输入框上面的文本控件 tv_message(用于作为hint位置、字号的占位)
输入框顶部的文本控件 tv_to_message(用于显示提示信息)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="10dip"
    android:paddingRight="10dip">
    <TextView
        android:id="@+id/tv_to_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:textColor="#999999"
        android:textSize="14sp"
        android:text="请输入手机号"
        android:visibility="visible"/>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:id="@+id/rl"
        android:layout_below="@+id/tv_to_message">

        <ImageView
            android:id="@+id/iv_phone_clear"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerInParent="true"
            android:src="@mipmap/close_white"
            android:visibility="invisible" />

        <EditText
            android:id="@+id/et_phone"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toLeftOf="@+id/iv_phone_clear"
            android:background="@null"
            android:inputType="phone"
            android:textColor="#2A2A2A"
            android:textColorHint="#999999"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/tv_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:clickable="false"
            android:textColor="#999999"
            android:gravity="center_vertical"
            android:layout_centerVertical="true"
            android:textSize="16sp"
            android:text="请输入手机号"
            android:visibility="invisible"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_alignParentBottom="true"
            android:background="#EBEBEB" />
    </RelativeLayout>
</RelativeLayout>

3 . 创建自定义控件类(定义成可new ,可直接在xml中使用的控件)

构造函数:
自定义控件,必须使用特定的构造函数:
1. 一个参数的构造函数,可用于其他代码中直接new 当前控件
  UserPhoneEditText(Context context)
2. 两个以上参数的构造函数,可用于直接在布局xml中使用当前控件,使用AttributeSet 可获得我们在xml中设置的属性;(后面有讲解)
UserPhoneEditText(Context context, AttributeSet attrs)

更多构造函数相关的信息,请自行查找资料!!!

代码中解析获得自定义参数:

//获得 在attrs.xml UserPhoneEditText中已定义的属性集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.user_phone_edittext);
showTopMessage = typedArray.getBoolean(R.styleable.user_phone_edittext_showTopMessage, false);
topMessage = typedArray.getString(R.styleable.user_phone_edittext_topMessage);
hint = typedArray.getString(R.styleable.user_phone_edittext_hint);
//释放
typedArray.recycle();

1. R.styleable.user_phone_edittext是我们在res/attrs.xml中定义的名称,对应自动生成的id
2. 获得参数后,一定记得把TypedArray 释放掉,切记!!!

创建自定义控件并获得自定义参数的详细代码:

/**
 * 类:UserPhoneEditText
 * 作者: qxc
 * 日期:2018/3/2.
 */
public class UserPhoneEditText extends RelativeLayout {
    private Context context;//上下文

    private boolean showTopMessage;//自定义属性:是否显示顶端提示信息(true:显示,false:不显示)
    private String topMessage;//自定义属性:顶端提示信息内容
    private String hint;//输入框提示信息

    private EditText et_phone;//电话号输入框
    private ImageView iv_phone_clear;//清空输入框的按钮
    private TextView tv_message;//输入框内的消息文本
    private TextView tv_to_message;//输入框外的消息文本

    public UserPhoneEditText(Context context) {
        super(context);
        this.context = context;
        LoadView(context);
    }

    public UserPhoneEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        getAttrs(context,attrs);
        LoadView(context);
    }

    /**
     * 获得配置的自定义属性
     * @param context 上下文
     * @param attrs 属性集合
     */
    private void getAttrs(Context context,AttributeSet attrs){
        //获得 在attrs.xml UserPhoneEditText中已定义的属性集合
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.user_phone_edittext);
        showTopMessage = typedArray.getBoolean(R.styleable.user_phone_edittext_showTopMessage, false);
        topMessage = typedArray.getString(R.styleable.user_phone_edittext_topMessage);
        hint = typedArray.getString(R.styleable.user_phone_edittext_hint);
        //释放
        typedArray.recycle();
    }

    /**
     * 初始化view
     * @param context 上下文
     */
    private void LoadView(Context context){
        View view = LayoutInflater.from(context).inflate(R.layout.user_phone_edittext, this);
        initView(view);//初始化组件
        initEvent();//初始化事件
    }

    /**
     * 初始化组件
     */
    private void initView(View view){
        et_phone = (EditText) view.findViewById(R.id.et_phone);
        iv_phone_clear = (ImageView) view.findViewById(R.id.iv_phone_clear);
        tv_message = (TextView) view.findViewById(R.id.tv_message);
        tv_to_message = (TextView) view.findViewById(R.id.tv_to_message);

        //根据自定义属性,显示组件
        //设置文本信息
        if(topMessage!=null){
            tv_to_message.setText(hint);
        }
    }

    /**
     * 初始化事件
     */
    private void initEvent(){
        //清空输入框内容
        iv_phone_clear.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                et_phone.setText("");
            }
        });
        //输入框内容变更事件
        //如果输入框开始输入字符,tv_message使用动画移动到tv_to_message的位置
        //如果输入框变成空,tv_message从tv_to_message的位置再移动回来
        et_phone.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }
            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }
            @Override
            public void afterTextChanged(Editable editable) {
                String text = editable.toString();
                if (text.length() == 0) {
                        //清空输入框
                        //执行动画

                    }
                }
                //如果输入框开始输入字符,tv_message使用动画移动到tv_to_message的位置
                else if (text.length() == 1 && tvPosition == 0) {
                        //输入框内容变化
                        //执行动画
                        //当输入完11位手机号后,执行结果回调
                }
            }
        });
    }
}

4 , 动画的执行

动画1:平移动画

需要获得两个坐标点:
坐标点1:hint文本的位置(tv_message)
坐标点2:消息文本的位置( tv_top_message)

咱们先定义两个数值,用于存放坐标点坐标
private int[] position1 = new int[2];//tv_message的默认位置坐标
private int[] position2 = new int[2];//tv_to_message的默认位置坐标

x要移动、y也要移动,所以使用动画集合AnimatorSet
如果仅是移动,代码如下:
AnimatorSet set = new AnimatorSet();
set.playTogether(
      ObjectAnimator.ofFloat(tv_to_message , "TranslationX" , startX , endX),
      ObjectAnimator.ofFloat(tv_to_message , "TranslationY" ,startY, endY));
set.setDuration(duration).start();

注意:
startX 、endX等值不是指屏幕上绝对的坐标地址(例如:坐标(200,200)),而是在x轴上平移的数值变化。
例如:
startX = 0 表示当前控件的X位置变化为0;
endX =100 表示从startX开始,向右移动100像素;
endX =-100 表示从startX开始,向左移动100像素;
ofFloat 后面还可以继续增加X的值,用于表示X轴上移动的路径过程。
我们实际startX 、endX值是由tv_message、tv_top_message的坐标的X相减得来的,也就是求的控件的相对距离,作为动画移动的距离或位置。

动画2:字号变化

需要获得两个文本的字号值(tv_message、tv_top_message)
咱们先定义一个数组,用于存放两个文本的字号值
private float[] fonts = new float[2];//tv_to_message的默认大小

动画的执行,如果是边移动边伸缩字号,可以继续使用AnimatorSet,代码也就改造成:
/**
     * 播放动画
     * @param startX 开始X
     * @param endX 目标X
     * @param startY 开始Y
     * @param endY 目标Y
     * @param startFont 开始字号
     * @param endFont 目标字号
     */
    private void startAnim(float startX, float endX, float startY, float endY, float startFont, float endFont){
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                ObjectAnimator.ofFloat(tv_to_message , "TranslationX" , startX , endX),
                ObjectAnimator.ofFloat(tv_to_message , "TranslationY" ,startY, endY),
                ObjectAnimator.ofFloat(tv_to_message , "TextSize" , startFont, endFont));
        set.setDuration(duration).start();
    }

重点:
自定义控件中,如何获取到tv_message、tv_top_message的坐标和字号大小呢??

自定义控件有自己的函数周期,不同的函数做不同的事情,
如onSizeChanged、onMeasure、onLayout、onDraw等。如果不明白这些方法是做什么的,请自行查找资料。

我们先来写个代码做个试验,先来看下自定义控件函数的执行顺序:
自定义个简单的view,测试代码:

package iwangzhe.testcustomview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
 * 类:TestView
 * 作者: qxc
 * 日期:2018/2/27.
 */
public class TestView extends View {
    final String Tag = "TestView";
    public TestView(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.i(Tag,"构造函数TestView");
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        Log.i(Tag,"onFinishInflate");
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.i(Tag,"onMeasure");
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.i(Tag,"onLayout");
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.i(Tag,"onSizeChanged");
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.i(Tag,"onDraw");
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(Tag,"onTouchEvent");
        invalidate();
        return super.onTouchEvent(event);
    }

    @Override
    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
        Log.i(Tag,"onFocusChanged");
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        Log.i(Tag,"onWindowFocusChanged");
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        Log.i(Tag,"onAttachedToWindow");
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        Log.i(Tag,"onDetachedFromWindow");
    }

    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);
        Log.i(Tag,"onWindowVisibilityChanged");
    }
}

输出结果:

03-05 17:38:55.690 23189-23189/iwangzhe.testcustomview I/TestView: 构造函数TestView
03-05 17:38:55.690 23189-23189/iwangzhe.testcustomview I/TestView: onFinishInflate
03-05 17:38:55.770 23189-23189/iwangzhe.testcustomview I/TestView: onAttachedToWindow
03-05 17:38:55.770 23189-23189/iwangzhe.testcustomview I/TestView: onWindowVisibilityChanged
03-05 17:38:55.780 23189-23189/iwangzhe.testcustomview I/TestView: onMeasure
03-05 17:38:55.780 23189-23189/iwangzhe.testcustomview I/TestView: onMeasure
03-05 17:38:55.820 23189-23189/iwangzhe.testcustomview I/TestView: onSizeChanged
03-05 17:38:55.820 23189-23189/iwangzhe.testcustomview I/TestView: onLayout
03-05 17:38:55.830 23189-23189/iwangzhe.testcustomview I/TestView: onDraw
03-05 17:38:55.860 23189-23189/iwangzhe.testcustomview I/TestView: onWindowFocusChanged
03-05 17:38:55.880 23189-23189/iwangzhe.testcustomview I/TestView: onMeasure
03-05 17:38:55.880 23189-23189/iwangzhe.testcustomview I/TestView: onMeasure
03-05 17:38:55.880 23189-23189/iwangzhe.testcustomview I/TestView: onLayout
03-05 17:38:55.880 23189-23189/iwangzhe.testcustomview I/TestView: onDraw
......(重复onMeasure、onLayout、onDraw)

我们看到,页面加载自定义控件,准备完毕后,会执行onWindowFocusChanged方法,那么这个方法之前,已经执行了初始化、计算、布局和绘制显示,控件的位置等信息已经被赋值。所以在onWindowFocusChanged方法中,我们是可以获取到相应属性的,代码如下:

/**
     * 自定义控件准备完毕,获得各组件的位置等数据
     */
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        //获得消息文本的位置信息
        tv_message.getLocationInWindow(position1);
        tv_to_message.getLocationOnScreen(position2);

        //获得消息文本的字号信息
        fonts[0] =  PxUtils.px2sp(context,tv_message.getTextSize());
        fonts[1] = PxUtils.px2sp(context,tv_to_message.getTextSize());

        //初始化位置、字号(把tv_to_message设置的与tv_message显示一致)
        tv_to_message.setTextSize(fonts[0]);
        tv_to_message.setTranslationX(position1[0]-position2[0]);
        tv_to_message.setTranslationY(position1[1]-position2[1]);
    }

5,手机号 3 -4 -4格式

代码比较简单,如下:

/**
     * 电话3 4 4格式(即:xxx xxxx xxxx)
     * 电话长度11位数字
     * @param view 输入框
     * @param text 文本
     */
    public static void onTextChanged344(EditText view, String text) {
        if (view== null || text == null || text.length() == 0) return;
        char space = ' ';
        int indexSpace1 = 3;
        int indexSpace2 = 8;
        StringBuilder sb = new StringBuilder();
        //1.取出所有字符,去掉' '和非法字符
        for (int i = 0; i < text.length(); i++) {
            //如果数字数大于11位,去掉后面的数字
            if(sb.length() >= 11){
                break;
            }

            //是否合法字符(0~9) (正则表达式)
            Pattern pattern = Pattern.compile("^[0-9]*$");
            Matcher matcher = pattern.matcher(String.valueOf(text.charAt(i)));
            if (text.charAt(i) != space && matcher.matches()) {
                sb.append(text.charAt(i));
            }
        }

        //2.根据长度追加' '
        if(sb.length() > indexSpace1){
            sb.insert(indexSpace1, space);
        }
        if(sb.length() > indexSpace2){
            sb.insert(indexSpace2, space);
        }
        //3.设置文本和光标位置
        if(!sb.toString().equals(text)){
            view.setText(sb.toString());
            view.setSelection(sb.length());
        }
    }

完整代码

上面,基本的技术点都解决了,那么我们把代码串起来,并贴出完整的代码吧(后面会给出Demo源码地址)

类1:PxUtils px与sp转换的帮助类

/**
 * 类:PxUtils
 * 作者: qxc
 * 日期:2018/3/5.
 */

public class PxUtils {
    /**
     * px转sp
     * @param context 上下文
     * @param pxValue px
     * @return sp
     */
    public static int px2sp(Context context, float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }
}

类2:电话号格式处理类

/**
 * 类:PhoneFormat
 * 作者: qxc
 * 日期:2018/3/5.
 */
public class PhoneFormat {
    /**
     * 电话3 4 4格式(即:xxx xxxx xxxx)
     * 电话长度11位数字
     * @param view 输入框
     * @param text 文本
     */
    public static void onTextChanged344(EditText view, String text) {
        if (view== null || text == null || text.length() == 0) return;
        char space = ' ';
        int indexSpace1 = 3;
        int indexSpace2 = 8;
        StringBuilder sb = new StringBuilder();
        //1.取出所有字符,去掉' '和非法字符
        for (int i = 0; i < text.length(); i++) {
            //如果数字数大于11位,去掉后面的数字
            if(sb.length() >= 11){
                break;
            }

            //是否合法字符(0~9)
            Pattern pattern = Pattern.compile("^[0-9]*$");
            Matcher matcher = pattern.matcher(String.valueOf(text.charAt(i)));
            if (text.charAt(i) != space && matcher.matches()) {
                sb.append(text.charAt(i));
            }
        }

        //2.根据长度追加' '
        if(sb.length() > indexSpace1){
            sb.insert(indexSpace1, space);
        }
        if(sb.length() > indexSpace2){
            sb.insert(indexSpace2, space);
        }
        //3.设置文本和光标位置
        if(!sb.toString().equals(text)){
            view.setText(sb.toString());
            view.setSelection(sb.length());
        }
    }

    /**
     * 获得已输入的电话号,不包括空格
     * @param editText 输入控件
     * @return 电话号
     */
    public static String getPhoneNumber(EditText editText){
        if (editText== null || editText.getText() == null) return "";
        String text = editText.getText().toString();
        char space = ' ';
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < text.length(); i++) {
            if (text.charAt(i) != space) {
                sb.append(text.charAt(i));
            }
        }
        return sb.toString();
    }
}

类3:自定义控件类(核心类)

/**
 * 类:UserPhoneEditText
 * 作者: qxc
 * 日期:2018/3/2.
 */
public class UserPhoneEditText extends RelativeLayout {
    private Context context;//上下文

    private boolean showTopMessage;//自定义属性:是否显示顶端提示信息(true:显示,false:不显示)
    private String topMessage;//自定义属性:顶端提示信息内容
    private String hint;//输入框提示信息

    private EditText et_phone;//电话号输入框
    private ImageView iv_phone_clear;//清空输入框的按钮
    private TextView tv_message;//输入框内的消息文本
    private TextView tv_to_message;//输入框外的消息文本

    private int duration = 200;//动画执行时间
    private int tvPosition = 0;//tv_message的当前位置,0:在输入框里;1:在tv_to_message的位置(执行动画前判断)
    private int[] position1 = new int[2];//tv_message的默认位置坐标
    private int[] position2 = new int[2];//tv_to_message的默认位置坐标
    private float[] fonts = new float[2];//tv_to_message的默认大小

    public UserPhoneEditText(Context context) {
        super(context);
        this.context = context;
        LoadView(context);
    }

    public UserPhoneEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        getAttrs(context,attrs);
        LoadView(context);
    }
    /**
     * 获得配置的自定义属性
     * @param context 上下文
     * @param attrs 属性集合
     */
    private void getAttrs(Context context,AttributeSet attrs){
        //获得 在attrs.xml UserPhoneEditText中已定义的属性集合
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.user_phone_edittext);
        showTopMessage = typedArray.getBoolean(R.styleable.user_phone_edittext_showTopMessage, false);
        topMessage = typedArray.getString(R.styleable.user_phone_edittext_topMessage);
        hint = typedArray.getString(R.styleable.user_phone_edittext_hint);
        //释放
        typedArray.recycle();
    }

    /**
     * 初始化view
     * @param context 上下文
     */
    private void LoadView(Context context){
        View view = LayoutInflater.from(context).inflate(R.layout.user_phone_edittext, this);
        initView(view);//初始化组件
        initEvent();//初始化事件
    }

    /**
     * 初始化组件
     */
    private void initView(View view){
        et_phone = (EditText) view.findViewById(R.id.et_phone);
        iv_phone_clear = (ImageView) view.findViewById(R.id.iv_phone_clear);
        tv_message = (TextView) view.findViewById(R.id.tv_message);
        tv_to_message = (TextView) view.findViewById(R.id.tv_to_message);

        //根据自定义属性,显示组件
        //设置文本信息
        if(topMessage!=null){
            tv_to_message.setText(hint);
        }
    }

    /**
     * 初始化事件
     */
    private void initEvent(){
        //清空输入框内容
        iv_phone_clear.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                et_phone.setText("");
            }
        });
        //输入框内容变更事件
        //如果输入框开始输入字符,tv_message使用动画移动到tv_to_message的位置
        //如果输入框变成空,tv_message从tv_to_message的位置再移动回来
        et_phone.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }
            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }
            @Override
            public void afterTextChanged(Editable editable) {
                String text = editable.toString();
                //如果输入框变成空,tv_message从tv_to_message的位置再移动回来
                if (text.length() == 0) {
                    iv_phone_clear.setVisibility(View.INVISIBLE);
                    //如果不显示顶部消息,也就不需要动画效果了
                    if(!showTopMessage) {
                        tv_to_message.setVisibility(VISIBLE);
                    }else {
                        tvPosition = 0;
                        float startX = 0;
                        float endX = position1[0] - position2[0];
                        float startY = 0;
                        float endY = position1[1] - position2[1];
                        //执行动画
                        startAnim(startX, endX, startY, endY, fonts[1], fonts[0]);
                        tv_to_message.setText(hint);
                    }
                }
                //如果输入框开始输入字符,tv_message使用动画移动到tv_to_message的位置
                else if (text.length() == 1 && tvPosition == 0) {
                    iv_phone_clear.setVisibility(View.VISIBLE);
                    if(!showTopMessage) {
                        tv_to_message.setVisibility(INVISIBLE);
                    }else {
                        tvPosition = 1;
                        float startX = position1[0] - position2[0];
                        float endX = 0;
                        float startY = position1[1] - position2[1];
                        float endY = 0;
                        //执行动画
                        startAnim(startX, endX, startY, endY, fonts[0], fonts[1]);
                        tv_to_message.setText(topMessage);
                    }
                }
                //344电话格式处理
                PhoneFormat.onTextChanged344(et_phone,editable.toString());
                //回调
                if(et_phone.getText().length()==13&&onSuccessListener!=null){
                    onSuccessListener.onSuccess(et_phone.getText().toString());
                }
            }
        });
    }

    /**
     * 自定义控件准备完毕,获得各组件的位置等数据
     */
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        //获得消息文本的位置信息
        tv_message.getLocationInWindow(position1);
        tv_to_message.getLocationOnScreen(position2);

        //获得消息文本的字号信息
        fonts[0] =  PxUtils.px2sp(context,tv_message.getTextSize());
        fonts[1] = PxUtils.px2sp(context,tv_to_message.getTextSize());

        //初始化位置、字号(把tv_to_message设置的与tv_message显示一致)
        tv_to_message.setTextSize(fonts[0]);
        tv_to_message.setTranslationX(position1[0]-position2[0]);
        tv_to_message.setTranslationY(position1[1]-position2[1]);
    }

    /**
     * 播放动画
     * @param startX 开始X
     * @param endX 目标X
     * @param startY 开始Y
     * @param endY 目标Y
     * @param startFont 开始字号
     * @param endFont 目标字号
     */
    private void startAnim(float startX, float endX, float startY, float endY, float startFont, float endFont){
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                ObjectAnimator.ofFloat(tv_to_message , "TranslationX" , startX , endX),
                ObjectAnimator.ofFloat(tv_to_message , "TranslationY" ,startY, endY),
                ObjectAnimator.ofFloat(tv_to_message , "TextSize" , startFont, endFont));
        set.setDuration(duration).start();
    }

    /**
     * 获得输入的电话号
     * @return 输入的电话号
     */
    public String getPhone(){
        return PhoneFormat.getPhoneNumber(et_phone);
    }

    /**
     * 获得输入的电话号,用于显示
     * @return 输入的电话号 334格式
     */
    public String getText(){
        return et_phone.getText().toString();
    }

    /**
     * 输入完成回调
     */
    public interface OnSuccessListener{
        /**
         * 输入完成
         * @param phone 电话号
         */
        void onSuccess(String phone);
    }
    private OnSuccessListener onSuccessListener;

    /**
     * 设置监听
     * @param onSuccessListener
     */
    public void setOnSuccessListener(OnSuccessListener onSuccessListener){
        this.onSuccessListener = onSuccessListener;
    }
}

布局:user_phone_edittext.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="10dip"
    android:paddingRight="10dip">
    <TextView
        android:id="@+id/tv_to_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:textColor="#999999"
        android:textSize="14sp"
        android:text="请输入手机号"
        android:visibility="visible"/>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:id="@+id/rl"
        android:layout_below="@+id/tv_to_message">
        <ImageView
            android:id="@+id/iv_phone_clear"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerInParent="true"
            android:src="@mipmap/close_white"
            android:visibility="invisible" />

        <EditText
            android:id="@+id/et_phone"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toLeftOf="@+id/iv_phone_clear"
            android:background="@null"
            android:inputType="phone"
            android:textColor="#2A2A2A"
            android:textColorHint="#999999"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/tv_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:clickable="false"
            android:textColor="#999999"
            android:gravity="center_vertical"
            android:layout_centerVertical="true"
            android:textSize="16sp"
            android:text="请输入手机号"
            android:visibility="invisible"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_alignParentBottom="true"
            android:background="#EBEBEB" />
    </RelativeLayout>
</RelativeLayout>自定义属性attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--用户-手机号输入控件-自定义属性-->
    <declare-styleable name="user.phone.edittext">
        <attr name="showTopMessage" format="boolean"/>
        <attr name="topMessage" format="string"/>
        <attr name="hint" format="string"/>
    </declare-styleable>
</resources>

测试类MainActivity(测试调用自定义控件)
.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:userphoneedittext="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="iwangzhe.testcustomview.MainActivity">

    <Button
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:id="@+id/btn2"
        android:text="B"
        android:visibility="gone"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:id="@+id/btn1"
        android:layout_below="@id/btn2"
        android:text="A"
        android:visibility="gone"/>

    <iwangzhe.testcustomview.TestView
        android:layout_below="@id/btn1"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:id="@+id/tv1"/>

    <iwangzhe.testcustomview.userphone.UserPhoneEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv1"
        android:id="@+id/upet1"
        userphoneedittext:showTopMessage="true"
        userphoneedittext:topMessage="测试消息信息"
        userphoneedittext:hint="默认hint消息信息">
    </iwangzhe.testcustomview.userphone.UserPhoneEditText>

    <Button
        android:text="验证手机号"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="18dp"
        android:id="@+id/btnPhone"
        android:layout_below="@+id/upet1"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

自定义属性的使用,需要先设置命名控件:xmlns:userphoneedittext="http://schemas.android.com/apk/res-auto";
userphoneedittext可以自己定义,其他格式固定。

public class MainActivity extends BaseActivity {

    Button btn1;
    Button btn2;
    Button btnPhone;
    UserPhoneEditText upet;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnPhone = (Button) findViewById(R.id.btnPhone);
        upet = (UserPhoneEditText) findViewById(R.id.upet1);
        //设置回调监听,获得输入完成的回调数据(被动回调)
        upet.setOnSuccessListener(new UserPhoneEditText.OnSuccessListener(){
            @Override
            public void onSuccess(String phone) {
                Toast.makeText(MainActivity.this, phone, Toast.LENGTH_SHORT).show();
            }
        });

        //获得自定义控件文本信息(主动获取)
        btnPhone.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String text = upet.getPhone();
                Toast.makeText(MainActivity.this,text,Toast.LENGTH_SHORT).show();
            }
        });
    }
}

还有不明白的请看Demo,如果还是不明白请留言或者自行查询资料。
本文章,主要是为了让大家了解自定义控件的过程,如果想在自己的项目中使用,请根据需要自行调整优化。

Demo地址:
https://pan.baidu.com/s/1g5Ro3ZUWcdLwQezSmYFrBA

往期干货

1

MPAndroidChart 绘制曲线图总结

2

【需求解决系列一】之移动卡片实现答题功能

3

【年后第一篇】如何在复杂业务场景中优雅实现Android指纹验证?

猜你喜欢

转载自blog.csdn.net/sinat_17775997/article/details/81156329
今日推荐