弹跳字符效果如下:
1)实时监听键盘字符输入,进而输入对应的字母;
2)为字符加入全局弹跳效果,字符分为上下进入两种情况;
3)定制弹跳字符的颜色、大小、进入时间、消失时间、不同的方向,不同的动效;
4)输入文字时,字符有跳跃效果;删除字符时,字符也要添加类似的动效;
1)监听键盘输入很容易想到TextWatcher()接口,自定义View-继承自EditText,实现该接口;
2)EditText的位置固定在某处 ,要实现添加全局的动态字符,需要获取到Activity的根布局,在根布局中进行添加;
3)字符进入时的动效如何实现,如何实现不同的动效;动效肯定采用属性动画实现,需要使用适当的插值器或者估值器;
4)弹跳字符的自定义,很明显需要为EditText添加自定义属性,进而去控制添加弹跳字符的各种效果;
5)输入文字和删除文字两种状态都可以在TextWatcher()接口中进行监听,可以写一个标记位加以区分,实现是类似的;
按照上述需求,一步步实现:
1)自定义View —— 继承自EditText,实现TextWatcher()接口;
private String cacheStr = "";
public class BiuEditText extends EditText {
public BiuEditText(Context context) {
super(context);
}
public BiuEditText(Context context, AttributeSet attrs) {
super(context, attrs);
setlistener();
}
public BiuEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 输入文字时的监听
* BiuEditText静态添加进XML布局 所以该监听方法写到第二个构造方法中
*/
private void setlistener() {
addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (cacheStr.length() < s.length()) {
// 输入文字时
char last = s.charAt(s.length() - 1);
update(last, false);
} else if (cacheStr.length() >= 1) {
// 删除文字时
char last = cacheStr.charAt(cacheStr.length() - 1);
update(last, true);
}
cacheStr = s.toString();
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
}
因为本例中BiuEditText是静态添加到XML布局中,所以监听方法实现写在第二个构造方法中,实现中,在onTextChanged()方法中,我们监听键盘的两种状态-输入字符和删除字符,并对应的传入不同的布尔标记位,加以区分两种状态。
2)为BiuEditText添加自定义属性;
private ViewGroup contentContainer;
private int height;
private int biuTextColor;
private float biuTextStartSize;
private float biuTextScale;
private int biuDuration;
private int biuType;
private void init(Context context, AttributeSet attrs) {
if (isInEditMode())
return;
if (null == attrs) {
throw new IllegalArgumentException("Attributes should be provided to this view,");
}
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BiuEditStyle);
biuTextColor = typedArray.getColor(R.styleable.BiuEditStyle_biu_text_color, getResources().getColor(R.color.white));
biuTextStartSize = typedArray.getDimension(R.styleable.BiuEditStyle_biu_text_start_size, getResources().getDimension(R.dimen.biu_text_start_size));
biuTextScale = typedArray.getFloat(R.styleable.BiuEditStyle_biu_text_scale, DEFAULT_SCALE);
biuDuration = typedArray.getInt(R.styleable.BiuEditStyle_biu_duration, DEFAULT_DURATION);
biuType = typedArray.getInt(R.styleable.BiuEditStyle_biu_type, 0);
typedArray.recycle();
/**
* 关键所在-通过findViewById(android.R.id.content)获取Activity根View布局 跳跃文字都是添加在该容器布局内部
* 所以点击键盘时,跳跃的TextView是出现在全局中,而不仅仅局限在自定义View所在的位置
*/
contentContainer = (ViewGroup) ((Activity) getContext()).findViewById(android.R.id.content);
Log.d("根布局id", "contentContainer: " + contentContainer.getId());
WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
height = windowManager.getDefaultDisplay().getHeight(); // 获取屏幕的高度 此方法已被废弃 改为 height = getContext().getResource().getDisplayMeytrics().heightPixels;
}
①根据之前的分析,我们为跳跃字符添加的自定义属性包括颜色、大小、时间和弹跳方向;
②定义init()方法,在里面实现自定义属性添加的逻辑,然后将该方法添加到三个构造方法中去,注意以下这行代码:
contentContainer = (ViewGroup) ((Activity) getContext()).findViewById(android.R.id.content);
这是整个自定义EditText的关键所在,如前面分析所述,我们添加的BiuEditText写入在XML布局中,固定在界面上某个位置,那如何实现全局动态添加字符?——关键就在于利用getContext()).findViewById(android.R.id.content)获取到Activity根布局,然后将动态字符TextVIew添加进该根布局中,就可以实现全局动态添加。图示如下:
参考文章:android获取根View的方法 获取activity的根布局 了解android.R.id.content
3)为BUIEditText增加添加动态字符的方法;
/**
* 动态添加或者动态删除跳跃的TextView
* @param last
* @param isOpposite
*/
private void update(char last, boolean isOpposite) {
final TextView textView = new TextView(getContext());
textView.setTextColor(Color.BLUE);
textView.setTextSize(biuTextStartSize);
textView.setText(String.valueOf(last));
textView.setGravity(Gravity.CENTER);
contentContainer.addView(textView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
textView.measure(0, 0);
playAnaimator(textView, isOpposite, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
contentContainer.removeView(textView);
}
});
}
①首先在update方法中动态创建TextView,用以承载一个个字符,然后取出自定义属性中定义好的属性一个个添加给TextView,实现动态字符的定制;
②然后调用根布局的addView()方法,将创建好的TextView,一一进行添加;
③最后定义动画接口,在动画结束时,即字符已经出现到一定时间之后,调用removeView()方法将TextView一一移除;
4)为TextView指定向上出现动画和向下出现动画;
private void playAnaimator(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
switch (biuType) {
case ANIMATION_DEFAULT:
playFlyUp(textView, isOpposite, listenerAdapter);
break;
case ANIMATION_DROPOUT:
playFlyDown(textView, isOpposite, listenerAdapter);
break;
default:
break;
}
}
/**
* TextView下落动画
* @param textView
* @param isOpposite
* @param listenerAdapter
*/
private void playFlyDown(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
int pos = getSelectionStart();
Layout layout = getLayout();
float startX = 0;
float startY = 0;
float endX = 0;
float endY = 0;
if (isOpposite) {
endX = new Random().nextInt(contentContainer.getWidth());
endY = 0;
startX = layout.getPrimaryHorizontal(pos) + 100;
startY = getY() - 100;
} else {
startX = layout.getPrimaryHorizontal(pos) + 100;
startY = -100;
endX = startX;
endY = getY() - 100;
}
final AnimatorSet animSet = new AnimatorSet();
ObjectAnimator animX = ObjectAnimator.ofFloat(textView, "translationX", startX, endX);
ObjectAnimator alpha = ObjectAnimator.ofFloat(textView, "alpha", 0, 1);
ObjectAnimator translationY = ObjectAnimator.ofFloat(textView, "translationY", startY, endY);
// 代码家的28种估值器之一 无比炫酷
translationY.setEvaluator(new BounceEaseInOut(biuDuration));
animSet.setDuration(biuDuration);
animSet.addListener(listenerAdapter);
animSet.playTogether(alpha, translationY, animX);
animSet.start();
}
/**
* TextView上浮动画
* @param textView
* @param isOpposite
* @param listenerAdapter
*/
private void playFlyUp(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
// TextView的getSelectionStart()方法用以获取光标的位置
int pos = getSelectionStart();
Layout layout = getLayout();
float startX = 0;
float startY = 0;
float endX = 0;
float endY = 0;
if (isOpposite) {
endX = new Random().nextInt(contentContainer.getWidth());
endY = height / 3 * 2;
startX = layout.getPrimaryHorizontal(pos) + 100;
startY = getY();
} else {
// layout.getPrimaryHorizontal获取光标左边的位置 再加上100px偏移量
startX = layout.getPrimaryHorizontal(pos) + 100;
// 屏幕高度的 2/3 这里可以改为用工具类获取键盘的高度
startY = height / 3 * 2;
endX = startX;
endY = getY();
}
final AnimatorSet animSet = new AnimatorSet();
ObjectAnimator animX = ObjectAnimator.ofFloat(textView, "translationX", startX, endX);
ObjectAnimator animY = ObjectAnimator.ofFloat(textView, "translationY", startY, endY);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(textView, "scaleX", 1f, biuTextScale);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(textView, "scaleY", 1f, biuTextScale);
animY.setEvaluator(new ExpoEaseInOut(biuDuration));
animY.setInterpolator(new DecelerateInterpolator());
animSet.setDuration(biuDuration);
animSet.addListener(listenerAdapter);
animSet.playTogether(animX, animY, scaleX, scaleY);
animSet.start();
}
向上向下动画实现类似,这里以向下动画playFlyDown为例,进行分析:
字符出现:①首先调用TextView的getSelectionStart()方法用以获取光标的位置,然后利用该位置计算对应的横向动画距离;
②然后利用此前计算好的屏幕高度,计算出竖直方向上的动画translationY距离;
③注意到竖直方向translationY设置了估值器:
translationY.setEvaluator(new BounceEaseInOut(biuDuration));
Android属性动画内置了10种插值器,3种估值器——IntEvaluator、FloatEvaluator、ArgbEvaluator,Evaluator作为一个动画转换器,它能把插值器计算得到小数转换成对应的数值位置,利用getAnimatedValue()方法获取的当前运动动画值就是由估值器进行转换得到的数值。
Evaluator在插值器之后进行计算,所以对动画的表现控制更为具体,本例中使用了代码家定义的28种估值器之一的BounceEaseInOut,实现下落时短暂的停顿效果。
④为动画组合添加之前定义好的监听器之后,最后调用AnimatorSet动画组合将动画一起执行;
⑤删除字符的动画也是一样实现,只是横向动画距离不再是固定的,而是随机生成的数值;
全部代码如下:直接在Activity布局文件中添加即可使用
package me.james.biuedittext;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.text.Editable;
import android.text.Layout;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.DecelerateInterpolator;
import android.widget.EditText;
import android.widget.TextView;
import java.util.Random;
import me.james.biuedittext.easing.bounce.BounceEaseInOut;
import me.james.biuedittext.easing.expo.ExpoEaseInOut;
/**
* 作者 james
* 分析 cpf
* 文件 BiuEditText
* 难点 在于如何实现全局添加动态跳跃的TextView
* 解决 通过findViewById(android.R.id.content)获取Activity根View布局 跳跃文字都是添加在该容器布局内部
*/
public class BiuEditText extends EditText {
private ViewGroup contentContainer;
private int height;
private String cacheStr = "";
private static final int ANIMATION_DEFAULT = 0;
private static final int ANIMATION_DROPOUT = 1;
private static final int DEFAULT_DURATION = 600;
private static final float DEFAULT_SCALE = 1.2f;
private int biuTextColor;
private float biuTextStartSize;
private float biuTextScale;
private int biuDuration;
private int biuType;
public BiuEditText(Context context) {
super(context);
}
public BiuEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
setlistener();
}
public BiuEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
if (isInEditMode())
return;
if (null == attrs) {
throw new IllegalArgumentException("Attributes should be provided to this view,");
}
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BiuEditStyle);
biuTextColor = typedArray.getColor(R.styleable.BiuEditStyle_biu_text_color, getResources().getColor(R.color.white));
biuTextStartSize = typedArray.getDimension(R.styleable.BiuEditStyle_biu_text_start_size, getResources().getDimension(R.dimen.biu_text_start_size));
biuTextScale = typedArray.getFloat(R.styleable.BiuEditStyle_biu_text_scale, DEFAULT_SCALE);
biuDuration = typedArray.getInt(R.styleable.BiuEditStyle_biu_duration, DEFAULT_DURATION);
biuType = typedArray.getInt(R.styleable.BiuEditStyle_biu_type, 0);
typedArray.recycle();
/**
* 关键所在-通过findViewById(android.R.id.content)获取Activity根View布局 跳跃文字都是添加在该容器布局内部
* 所以点击键盘时,跳跃的TextView是出现在全局中,而不仅仅局限在自定义View所在的位置
*/
contentContainer = (ViewGroup) ((Activity) getContext()).findViewById(android.R.id.content);
Log.d("根布局id", "contentContainer: " + contentContainer.getId());
WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
height = windowManager.getDefaultDisplay().getHeight(); // 获取屏幕的高度 此方法已被废弃 改为 height = getContext().getResource().getDisplayMeytrics().heightPixels;
}
/**
* 输入文字时的监听
* BiuEditText静态添加进XML布局 所以该监听方法写到第二个构造方法中
*/
private void setlistener() {
addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (cacheStr.length() < s.length()) {
// 输入文字时
char last = s.charAt(s.length() - 1);
update(last, false);
} else if (cacheStr.length() >= 1) {
// 删除文字时
char last = cacheStr.charAt(cacheStr.length() - 1);
update(last, true);
}
cacheStr = s.toString();
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
/**
* 动态添加或者动态删除跳跃的TextView
* @param last
* @param isOpposite
*/
private void update(char last, boolean isOpposite) {
final TextView textView = new TextView(getContext());
textView.setTextColor(Color.BLUE);
textView.setTextSize(biuTextStartSize);
textView.setText(String.valueOf(last));
textView.setGravity(Gravity.CENTER);
contentContainer.addView(textView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
textView.measure(0, 0);
playAnaimator(textView, isOpposite, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
contentContainer.removeView(textView);
}
});
}
private void playAnaimator(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
switch (biuType) {
case ANIMATION_DEFAULT:
playFlyUp(textView, isOpposite, listenerAdapter);
break;
case ANIMATION_DROPOUT:
playFlyDown(textView, isOpposite, listenerAdapter);
break;
default:
break;
}
}
/**
* TextView下落动画
* @param textView
* @param isOpposite
* @param listenerAdapter
*/
private void playFlyDown(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
int pos = getSelectionStart();
Layout layout = getLayout();
float startX = 0;
float startY = 0;
float endX = 0;
float endY = 0;
if (isOpposite) {
endX = new Random().nextInt(contentContainer.getWidth());
endY = 0;
startX = layout.getPrimaryHorizontal(pos) + 100;
startY = getY() - 100;
} else {
startX = layout.getPrimaryHorizontal(pos) + 100;
startY = -100;
endX = startX;
endY = getY() - 100;
}
final AnimatorSet animSet = new AnimatorSet();
ObjectAnimator animX = ObjectAnimator.ofFloat(textView, "translationX", startX, endX);
ObjectAnimator alpha = ObjectAnimator.ofFloat(textView, "alpha", 0, 1);
ObjectAnimator translationY = ObjectAnimator.ofFloat(textView, "translationY", startY, endY);
// 代码家的28种估值器之一 无比炫酷
translationY.setEvaluator(new BounceEaseInOut(biuDuration));
animSet.setDuration(biuDuration);
animSet.addListener(listenerAdapter);
animSet.playTogether(alpha, translationY, animX);
animSet.start();
}
/**
* TextView上浮动画
* @param textView
* @param isOpposite
* @param listenerAdapter
*/
private void playFlyUp(TextView textView, boolean isOpposite, AnimatorListenerAdapter listenerAdapter) {
// TextView的getSelectionStart()方法用以获取光标的位置
int pos = getSelectionStart();
Layout layout = getLayout();
float startX = 0;
float startY = 0;
float endX = 0;
float endY = 0;
if (isOpposite) {
endX = new Random().nextInt(contentContainer.getWidth());
endY = height / 3 * 2;
startX = layout.getPrimaryHorizontal(pos) + 100;
startY = getY();
} else {
// layout.getPrimaryHorizontal获取光标左边的位置 再加上100px偏移量
startX = layout.getPrimaryHorizontal(pos) + 100;
// 屏幕高度的 2/3 这里可以改为用工具类获取键盘的高度
startY = height / 3 * 2;
endX = startX;
endY = getY();
}
final AnimatorSet animSet = new AnimatorSet();
ObjectAnimator animX = ObjectAnimator.ofFloat(textView, "translationX", startX, endX);
ObjectAnimator animY = ObjectAnimator.ofFloat(textView, "translationY", startY, endY);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(textView, "scaleX", 1f, biuTextScale);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(textView, "scaleY", 1f, biuTextScale);
animY.setEvaluator(new ExpoEaseInOut(biuDuration));
animY.setInterpolator(new DecelerateInterpolator());
animSet.setDuration(biuDuration);
animSet.addListener(listenerAdapter);
animSet.playTogether(animX, animY, scaleX, scaleY);
animSet.start();
}
}
地址:https://github.com/daimajia/AnimationEasingFunctions
translationY.setEvaluator(new BounceEaseInOut(biuDuration));
本例中,单独将一些必要的文件(BaseEasingMethod+BounceEaseInOut)抽取出来,单独使用一个估值器:BounceEaseInOut;
若要使用其他估值器也是采用一样的方式,感兴趣的读者可以动手尝试下,28种强大的估值器,对动画的控制更为直接。
源代码出处:
BuiEdText:BiuEditText
代码家估值器:AnimationEasingFunctions
感谢开源!