国产动画音乐分享组

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

Android技术杂货铺 2019-04-14 12:04:19

点击上方“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指纹验证?


如果你觉得本文对你有帮助,请分享给更多的人 

 关注【Android技术杂货铺】,每天都有Android 干货文章分享!

长按识别二维码关注

Copyright © 国产动画音乐分享组@2017