一、简介
很久没有写博客了,前段时间一直忙着找工作面试,上个月总算找到了合适的工作,算是稳定下来了。也有时间去总结学习些东西。
二、需求
最近刚刚接到一个需求,交互那边要求实现一个类似于Android Material Design 里的一个TextInputLayout 的输入框动效交互。涉及到提示文字的动效、输入框的焦点变化导致的文字、输入框的UI变化等,看了下原生的TextInputLayout 的API,发现留给开发者自定义扩展API的太少了,无法满足设计那边设定的颜色、大小等一堆定制点的改造。本想着继承TextInputLayout来实现下,一看这个类2000多行。。。算了,我还是自己造轮子吧。
三、造轮子
先看看Android原生提供的TextInputLayout实现的效果:
看到这个交互,有三点需要实现。
- 自定义设置输入框。原生的EditText里的提示文字是不是View。
- 监听焦点状态和动画完结后设置输入框的状态。
- 提示文字的两个动画效果,即有焦点平移、缩小和无焦点的平移放大。
就这上面的这三点,我们逐一处理。 首先是自定义设置输入框。因为要涉及到提示文字的动效,因此这里的提示文字没有使用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);
}
}
最后就是那个动画的实现,其实就是一个组合动画而已。
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();
}
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;
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;
}
public ImageView getFunIcon(){
return mIvFun;
}
public void setFunIcon(int drawableId){
mIvFun.setImageResource(drawableId);
}
public void setUseClean(boolean state){
mUseClean = state;
}
public void setUseFun(boolean state){
if (state){
mIvFun.setVisibility(VISIBLE);
}else {
mIvFun.setVisibility(GONE);
}
}
public void setTextWatcher(TextWatcher textWatcher){
mTextWatcher = textWatcher;
}
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);
}
}
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();
}
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了,这次突然写很多东西都忘了,让我着实尴尬,哈哈哈哈。
|