Android自定义控件:带动画效果的手机号输入框 (3-4-4
项目中很多地方,使用到了自定义控件。
-
简单点的,如个性控件的定制,多个组件的组合封装等。
我们需要了解自定义控件的基础知识,即可快速实现; -
复杂点的,如各种图形报表(例如:股票K线图、分时图控件)。
我们除了自定义控件的基础知识,
还需要掌握控件事件的拦截传递机制,事件回调、手势识别、画图、 动画等技术;
还需要架构设计相关的思想。
关于自定义控件,我们逐步深入讲解:
今天,我们先来实现一个简单的自定义控件,后期找时间再讲解股票K线图、分时图控件如何自定义。
需求
实现带动画效果的手机号输入框:
1.输入手机号格式为3-4-4;
2.输入框中默认有hint提示,当开始输入数字时,有动画效果:
a) hint平移出输入框,停留在输入框上方指定位置,显示对应的信息;
b) 平移过程中,文字也逐渐由大变小;
3.当清空输入框,反效果动画;
4.输入时自动做数字格式校验(非数字不让输入)和长度校验(最多11位手机号)
5.当输入框有值后,最右边出现清空按钮,点击清空输入框
6.输入完成,回调结果;
需求效果.gif
分析
该输入框效果在多个页面中都会使用到,我们必须对其进行封装,此处最好的封装方案就是自定义控件。
我们APP中,所有页面的手机号输入框输入逻辑完全一样,但是个别页面存在小差异(个别页面输入手机号时,不需要动画效果,或者hint内容、提示消息不一样等等);
- 差异项应可配置:
自定义控件的以下内容应设计成可以配置的属性:
是否有动画效果;
hint文本;
hint移动到上部显示的文本等;
此处的重点:
1. 自定义属性如何配置?如何使用?
2.自定义控件被调用(使用)
应支持在代码中直接new 我们的控件
应支持在布局xml中直接使用我们的控件,可配置自定义属性
3.动画
平移效果:Tween动画、属性动画均可实现;
字体伸缩:应使用属性动画,根据字号去伸缩,宽高也会自动变化(注意:Tween动画无法做字号差值变化)
综上所述,应统一使用属性动画实现平移和伸缩的效果,而多个动画同时触发,会用到动画集合;
此处的重点:
1. 平移需要原始点、目标点两个坐标(x,y),自定义控件中如何获得对应的值?
2. 字体伸缩,需要伸缩前后的两个字号值,代码中默认获得的字号是px格式,如何与sp转换?
3. 设计点:要实现文本移动和字号伸缩的动画效果,我们可以在布局中放置2个文本控件,
tv_message:作为hint占位,不显示,仅用于获得坐标和字号;
tv_to_message: 作为顶部消息显示,作为hint显示,动画执行在该控件上;
当然,如何设计这种动画效果,还有很多其他的方式,大家使用时,可以根据自己的需要,合理设计。
- 手机号的3-4-4格式,就是拦截输入事件,处理字符串,没什么技术点;
- 手机号长度、特殊字符禁止输入验证,可使用正则表达式判断非法字符;
- 其他:略
技术实现分析
- 属性的定义,需要单独定义在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 //自定义属性:输入框提示信息
- 自定义控件的布局
使用相对布局,内容包括:
输入框 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位手机号后,执行结果回调
}
}
});
}
}
- 动画的执行
动画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]);
}
- 手机号 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转换的帮助类
package iwangzhe.testcustomview.userphone;
import android.content.Context;
/**
* 类: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:电话号格式处理类
package iwangzhe.testcustomview.userphone;
import android.widget.EditText;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 类: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:自定义控件类(核心类)
package iwangzhe.testcustomview.userphone;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import iwangzhe.testcustomview.R;
/**
* 类: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>
4 测试类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可以自己定义,其他格式固定。
package iwangzhe.testcustomview;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import org.greenrobot.eventbus.EventBus;
import iwangzhe.testcustomview.userphone.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,如果还是不明白请留言或者自行查询资料。
本文章,主要是为了让大家了解自定义控件的过程,如果想在自己的项目中使用,请根据需要自行调整优化。