Android 自定义View实现TextInputLayout——CofferTextInputLayout

一、简介

很久没有写博客了,前段时间一直忙着找工作面试,上个月总算找到了合适的工作,算是稳定下来了。也有时间去总结学习些东西。

二、需求

最近刚刚接到一个需求,交互那边要求实现一个类似于Android Material Design 里的一个TextInputLayout 的输入框动效交互。涉及到提示文字的动效、输入框的焦点变化导致的文字、输入框的UI变化等,看了下原生的TextInputLayout 的API,发现留给开发者自定义扩展API的太少了,无法满足设计那边设定的颜色、大小等一堆定制点的改造。本想着继承TextInputLayout来实现下,一看这个类2000多行。。。算了,我还是自己造轮子吧。

三、造轮子

先看看Android原生提供的TextInputLayout实现的效果:
在这里插入图片描述

看到这个交互,有三点需要实现。

  1. 自定义设置输入框。原生的EditText里的提示文字是不是View。
  2. 监听焦点状态和动画完结后设置输入框的状态。
  3. 提示文字的两个动画效果,即有焦点平移、缩小和无焦点的平移放大。

就这上面的这三点,我们逐一处理。
首先是自定义设置输入框。因为要涉及到提示文字的动效,因此这里的提示文字没有使用EditText里提供的,而是用TextView + EditText。代码如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_marginBottom="8dp"
        android:textSize="18sp"
        android:textStyle="bold"
        android:visibility="visible" />

    <EditText
        android:id="@+id/edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:paddingBottom="8dp"
        android:importantForAutofill="no"
        android:background="@drawable/edittext_bg_selector"
        android:textCursorDrawable="@drawable/edit_cursor_drawable"
        android:textSize="18sp"
        android:textStyle="bold" />
</FrameLayout>

接下来就是实现焦点、文字变化状态的监听。
这里会涉及到两个接口类TextChangedListener、OnFocusChangeListener。

mEditText.addTextChangedListener(new DefaultTextWatcher());
mEditText.setOnFocusChangeListener(new DefaultOnFocusChangeListener());

private class DefaultTextWatcher implements TextWatcher{

        @Override
        public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {

            if (mTextWatcher != null){
                mTextWatcher.beforeTextChanged(charSequence,start,count,after);
            }
        }

        @Override
        public void onTextChanged(CharSequence charSequence, int start, int before, int count) {

            if (mTextWatcher != null){
                mTextWatcher.onTextChanged(charSequence,start,before,count);
            }
        }

        @Override
        public void afterTextChanged(Editable editable) {
            String content = editable.toString();
            if (mUseClean){
                if (TextUtils.isEmpty(content)){
                    mIvClose.setVisibility(GONE);
                }else {
                    mIvClose.setVisibility(VISIBLE);
                }
            }
            if (mTextWatcher != null){
                mTextWatcher.afterTextChanged(editable);
            }
        }
    }

    private class DefaultOnFocusChangeListener implements OnFocusChangeListener{

        @Override
        public void onFocusChange(View view, boolean hasFocus) {
            String content = "";
            if (view instanceof EditText){
                content = ((EditText) view).getText().toString();
            }
            if (TextUtils.isEmpty(content)){
                if (hasFocus){
                    // 有焦点,给提示文案做右上角缩放动画
                    showFocusAnim();
                }else {
                    // 没有焦点,上面的动画反过来
                    showNoFocusAnim();
                }
            }

            if (mOnFocusChangeListener != null){
                mOnFocusChangeListener.onFocusChange(view,hasFocus);
            }
        }

最后就是那个动画的实现,其实就是一个组合动画而已。

/**
         * 给提示文案做动画
         * 动画效果:
         * 进行:1、View从左下角上移,2、View变小。变化时间200ms
         * 结束:1、文字颜色变化为输入框之外的颜色,2、文字字体变细(Normal)
         */
        private void showFocusAnim(){
            PropertyValuesHolder[] focus = new PropertyValuesHolder[]{
                  PropertyValuesHolder.ofFloat("scaleX",1f,0.67f),
                  PropertyValuesHolder.ofFloat("scaleY",1f,0.67f),
                  PropertyValuesHolder.ofFloat("translationY",0,
                          -Util.dipToPixel(getContext(),19))
            };
            ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mTvTip, focus);
            objectAnimator.setDuration(200);
            mTvTip.setPivotX(0);
            mTvTip.setPivotY(0);
            objectAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mTvTip.setTypeface(null,Typeface.NORMAL);
                    mTvTip.setTextColor(mHintTextTipColor);
                }
            });
            objectAnimator.start();
        }

        /**
         * 给提示文案做动画
         * 动画效果:
         * 进行:1、View从左上角下移,2、View变大。变化时间200ms
         * 结束:1、文字颜色变化为默认颜色,2、文字字体变粗(Bold)
         */
        private void showNoFocusAnim(){
            PropertyValuesHolder[] noFocus = new PropertyValuesHolder[]{
                PropertyValuesHolder.ofFloat("scaleX",0.67f,1f),
                PropertyValuesHolder.ofFloat("scaleY",0.67f,1f),
                PropertyValuesHolder.ofFloat("translationY",mTvTip.getTranslationY(),0)
            };
            ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mTvTip, noFocus);
            objectAnimator.setDuration(200);
            mTvTip.setPivotX(0);
            mTvTip.setPivotY(0);
            objectAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mTvTip.setTypeface(null,Typeface.BOLD);
                    mTvTip.setTextColor(mHintTextDefaultColor);
                }
            });
            objectAnimator.start();
        }
    }

最后给大家看下我们最终实现的效果:
在这里插入图片描述

四、完整的源码分享

package coffer.widget;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import coffer.androidjatpack.R;
import coffer.util.Util;

/**
 * author      : coffer
 * date        : 8/26/21
 * description : 实现交互类似于{@link com.google.android.material.textfield.TextInputLayout}的效果,
 *               TextInputLayout 存在的问题是对外暴露修改的API太少,留给开发者自定义扩展的场景也少,
 *               例如默认提示文案的文字大小、颜色,选中后的颜色,动画的时间等。
 * Reviewer    :
 */
public class CofferTextInputLayout extends RelativeLayout {

    private static final String TAG = "C_INPUT";

    /**
     * 自定义焦点发生改变的回调
     */
    private OnFocusChangeListener mOnFocusChangeListener;
    /**
     * 自定义输入框里的内容状态监听
     */
    private TextWatcher mTextWatcher;
    /**
     * 提示文案默认颜色(在输入框里)
     */
    private int mHintTextDefaultColor;
    /**
     * 提示文案真正提示状态的颜色(在输入框外)
     */
    private int mHintTextTipColor;
    /**
     * 是否使用清空内容
     */
    private boolean mUseClean;


    private EditText mEditText;
    private TextView mTvTip;
    private ImageView mIvClose;
    private ImageView mIvFun;

    public CofferTextInputLayout(Context context) {
        this(context,null);
    }

    public CofferTextInputLayout(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CofferTextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context,attrs);
    }

    private void init(Context context,AttributeSet attrs){
        LayoutInflater.from(context).inflate(R.layout.text_input_edit_layout, this);
        mEditText = findViewById(R.id.edit);
        mTvTip = findViewById(R.id.tip);
        mIvClose = findViewById(R.id.close);
        mIvFun = findViewById(R.id.fun);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CofferTextInputLayout);
        String hintText = typedArray.getString(R.styleable.CofferTextInputLayout_hintTipText);
        mHintTextTipColor = typedArray.getColor(R.styleable.CofferTextInputLayout_hintSelectTextColor,
                context.getResources().getColor(R.color.blue));
        mHintTextDefaultColor = typedArray.getColor(R.styleable.CofferTextInputLayout_hintTextDefaultColor,
                context.getResources().getColor(R.color.text_color_cr_reduce));
        mUseClean = typedArray.getBoolean(R.styleable.CofferTextInputLayout_useClean,false);
        mTvTip.setText(hintText);
        mTvTip.setTextColor(mHintTextDefaultColor);
        typedArray.recycle();
        // 设定属性,绑定相关的监听
        mEditText.addTextChangedListener(new DefaultTextWatcher());
        mEditText.setOnFocusChangeListener(new DefaultOnFocusChangeListener());
        mIvClose.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                mEditText.setText("");
            }
        });

    }

    public void setHintTextDefaultColor(int defaultColor){
        mHintTextDefaultColor = defaultColor;
    }

    public void setHintTextTipColor(int color){
        mHintTextTipColor = color;
    }

    public EditText getEditText(){
        return mEditText;
    }

    /**
     * 获取输入框最右边的功能图标
     * @return view
     */
    public ImageView getFunIcon(){
        return mIvFun;
    }

    /**
     * 设置输入框最右边的功能图标
     */
    public void setFunIcon(int drawableId){
        mIvFun.setImageResource(drawableId);
    }

    /**
     * 是否设置显示清空内容,功能图标和清空按钮不能同时显示
     * @param state 默认不显TRUE 表示使用
     */
    public void setUseClean(boolean state){
        mUseClean = state;
    }

    /**
     * 是否设置显示功能图标,功能图标和清空按钮不能同时显示
     * @param state TRUE 表示使用
     */
    public void setUseFun(boolean state){
        if (state){
            mIvFun.setVisibility(VISIBLE);
        }else {
            mIvFun.setVisibility(GONE);
        }
    }

    /**
     * 给外面自定义设置
     * @param textWatcher 内容监听
     */
    public void setTextWatcher(TextWatcher textWatcher){
        mTextWatcher = textWatcher;
    }

    /**
     * 给外面自定义设置
     * @param onFocusChangeListener 焦点监听
     */
    public void settOnFocusChangeListener(OnFocusChangeListener onFocusChangeListener){
        mOnFocusChangeListener = onFocusChangeListener;
    }

    private class DefaultTextWatcher implements TextWatcher{

        @Override
        public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {

            if (mTextWatcher != null){
                mTextWatcher.beforeTextChanged(charSequence,start,count,after);
            }
        }

        @Override
        public void onTextChanged(CharSequence charSequence, int start, int before, int count) {

            if (mTextWatcher != null){
                mTextWatcher.onTextChanged(charSequence,start,before,count);
            }
        }

        @Override
        public void afterTextChanged(Editable editable) {
            String content = editable.toString();
            if (mUseClean){
                if (TextUtils.isEmpty(content)){
                    mIvClose.setVisibility(GONE);
                }else {
                    mIvClose.setVisibility(VISIBLE);
                }
            }
            if (mTextWatcher != null){
                mTextWatcher.afterTextChanged(editable);
            }
        }
    }

    private class DefaultOnFocusChangeListener implements OnFocusChangeListener{

        @Override
        public void onFocusChange(View view, boolean hasFocus) {
            String content = "";
            if (view instanceof EditText){
                content = ((EditText) view).getText().toString();
            }
            if (TextUtils.isEmpty(content)){
                if (hasFocus){
                    // 有焦点,给提示文案做右上角缩放动画
                    showFocusAnim();
                }else {
                    // 没有焦点,上面的动画反过来
                    showNoFocusAnim();
                }
            }

            if (mOnFocusChangeListener != null){
                mOnFocusChangeListener.onFocusChange(view,hasFocus);
            }
        }

        /**
         * 给提示文案做动画
         * 动画效果:
         * 进行:1、View从左下角上移,2、View变小。变化时间200ms
         * 结束:1、文字颜色变化为输入框之外的颜色,2、文字字体变细(Normal)
         */
        private void showFocusAnim(){
            PropertyValuesHolder[] focus = new PropertyValuesHolder[]{
                  PropertyValuesHolder.ofFloat("scaleX",1f,0.67f),
                  PropertyValuesHolder.ofFloat("scaleY",1f,0.67f),
                  PropertyValuesHolder.ofFloat("translationY",0,
                          -Util.dipToPixel(getContext(),19))
            };
            ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mTvTip, focus);
            objectAnimator.setDuration(200);
            mTvTip.setPivotX(0);
            mTvTip.setPivotY(0);
            objectAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mTvTip.setTypeface(null,Typeface.NORMAL);
                    mTvTip.setTextColor(mHintTextTipColor);
                }
            });
            objectAnimator.start();
        }

        /**
         * 给提示文案做动画
         * 动画效果:
         * 进行:1、View从左上角下移,2、View变大。变化时间200ms
         * 结束:1、文字颜色变化为默认颜色,2、文字字体变粗(Bold)
         */
        private void showNoFocusAnim(){
            PropertyValuesHolder[] noFocus = new PropertyValuesHolder[]{
                PropertyValuesHolder.ofFloat("scaleX",0.67f,1f),
                PropertyValuesHolder.ofFloat("scaleY",0.67f,1f),
                PropertyValuesHolder.ofFloat("translationY",mTvTip.getTranslationY(),0)
            };
            ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mTvTip, noFocus);
            objectAnimator.setDuration(200);
            mTvTip.setPivotX(0);
            mTvTip.setPivotY(0);
            objectAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mTvTip.setTypeface(null,Typeface.BOLD);
                    mTvTip.setTextColor(mHintTextDefaultColor);
                }
            });
            objectAnimator.start();
        }
    }

}

 <!-- TextInput的属性-->
    <declare-styleable name="CofferTextInputLayout">
        <attr name="hintTextDefaultColor" format="color"/>
        <attr name="hintSelectTextColor" format="color"/>
        <attr name="hintTipText" format="string"/>
        <attr name="useClean" format="boolean"/>
    </declare-styleable>
 <coffer.widget.CofferTextInputLayout
        android:id="@+id/c_input_number"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        app:hintSelectTextColor="@color/blue"
        app:hintTextDefaultColor="@color/text_color_cr_reduce"
        app:hintTipText="哈哈" />

五、总结

关于扩展属性,我这里偷懒了,扩展的不多。如果大家有需要可以自行添加,对比原生的复杂实现,我的代码量量只有原生的10%左右,当然,原生的功能会更加强大、多,这个就看个人需求吧。
我差不多有一年没有写一个完整的自定义View了,这次突然写很多东西都忘了,让我着实尴尬,哈哈哈哈。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值