Android EditText 添加烟花效果

摆脱枯燥的文字输入,让输入更加炫彩。   老规矩先上图。

Android EditText 添加烟花效果 - 阿里云

难点

难点一:获取光标的坐标

难点二:烟花动画实现

光标坐标的计算

我们发现 api里并没有可以直接获取光标坐标的方法。api没有并不是说就没有。源码里肯定有,不然他光标是怎么画出来的呢。对吧。打开EditView的源码,只有一百多行,里面并没有关于光标的代码,那只好找他爸爸了—TextView。打开吓一跳,一万多行的代码,看源码讲究根据蛛丝马迹来推算。光标的英文是cursor。

Android EditText 添加烟花效果 - 阿里云

最终我们看到了

invalidateCursorPath()->invalidateCursor()->invalidateCursor(where, where, where)->invalidateRegion(start, end,true/* Also invalidates blinking cursor */);

终于找到了 这个方法invalidateRegion。

普及一下 android 字体的测量知识。

Android EditText 添加烟花效果 - 阿里云
光标的测量原理也是如此。我们需要得到光标的left和top的值,在加上padding的left和top值,就是我们光标在EditView里的偏移量了。

invalidate(bounds.left+ horizontalPadding, bounds.top+ verticalPadding,
bounds.right+ horizontalPadding, bounds.bottom+ verticalPadding);

我们寻找的偏移量 

XOffset = bounds.left+ horizontalPadding=bounds.left+getCompoundPaddingLeft();
YOffset = bounds.bottom+ verticalPadding=bounds.bottom+getExtendedPaddingTop() + getVerticalOffset(true);

反射取值

Class clazz = EditText.class;
clazz = clazz.getSuperclass();
try{
Field editor = clazz.getDeclaredField("mEditor");
editor.setAccessible(true);
Object mEditor = editor.get(mEditText);
Class editorClazz = Class.forName("android.widget.Editor");
Field drawables = editorClazz.getDeclaredField("mCursorDrawable");
drawables.setAccessible(true);
Drawable[] drawable= (Drawable[]) drawables.get(mEditor);
Method getVerticalOffset = clazz.getDeclaredMethod("getVerticalOffset",boolean.class);
Method getCompoundPaddingLeft = clazz.getDeclaredMethod("getCompoundPaddingLeft");
Method getExtendedPaddingTop = clazz.getDeclaredMethod("getExtendedPaddingTop");
getVerticalOffset.setAccessible(true);
getCompoundPaddingLeft.setAccessible(true);
getExtendedPaddingTop.setAccessible(true);
if(drawable !=null){
Rect bounds = drawable[0].getBounds();
Log.d(TAG,bounds.toString());
xOffset = (int) getCompoundPaddingLeft.invoke(mEditText) + bounds.left;
yOffset = (int) getExtendedPaddingTop.invoke(mEditText) + (int)getVerticalOffset.invoke(mEditText,false)+bounds.bottom;
}
}catch(NoSuchMethodException e) {
e.printStackTrace();
}catch(InvocationTargetException e) {
e.printStackTrace();
}catch(IllegalAccessException e) {
e.printStackTrace();
}catch(NoSuchFieldException e) {
e.printStackTrace();
}catch(ClassNotFoundException e) {
e.printStackTrace();
}
floatx =mEditText.getX() + xOffset;
floaty =mEditText.getY() + yOffset;
到目前位置 我们已经解决第一个难题了。好接下是烟花动画绘制部分。

烟花动画

烟花粒子
烟花
自定义View

烟花粒子

public class Element {
public int color;//颜色
public Double direction;//方向
public float speed;//速度
public float x;//坐标
public float y;
public Element(int color, Double direction, float speed) {
super();
this.color = color;
this.direction = direction;
this.speed = speed;
}

烟花

public class FireWork {
private final String TAG = this.getClass().getSimpleName();
private final static int DEFAULT_ELEMENT_COUNT = 12;// 默认 粒子的数量
private final static float DEFAULT_ELEMENT_SIZE = 8;// 默认 粒子的尺寸
private final static int DEFAULT_DURATION = 400;// 默认 动画间隔时间
private final static float DEFAULT_LAUNCH_SPEED = 18;// 默认 粒子 加载时的 速度
private final static float DEFAULT_WIND_SPEED = 6;// 默认 风的 素的
private final static float DEFAULT_GRAVITY = 6;// 默认 重力大小
private Paint mPaint;// 画笔
private int count;// 粒子数量
private int duration;// 间隔时间
private int[] colors;// 颜色库
private int color;
private float launchSpeed;
private int windDirection;// 1 or -1
private float windSpeed;
private float grivaty;
private Location location;
private float elemetSize;
private ValueAnimator animator;
private float animatorValue;
private ArrayList elements = new ArrayList ();
private AnimationEndListener listener;
public FireWork(Location location, int windDirection) {
this.location = location;
this.windDirection = windDirection;
colors = baseColors;
duration = DEFAULT_DURATION;
grivaty = DEFAULT_GRAVITY;
elemetSize = DEFAULT_ELEMENT_SIZE;
launchSpeed = DEFAULT_LAUNCH_SPEED;
windSpeed = DEFAULT_WIND_SPEED;
count = DEFAULT_ELEMENT_COUNT;
init();
}
private void init() {
Random random = new Random();
color = colors[random.nextInt(colors.length)];
// 给每一个火花 设定一个随机的方向 0 – 180
for (int i = 0; i < count; i++) {
elements.add(new Element(color, Math.toRadians(random.nextInt(180)), random.nextFloat() * launchSpeed));
}
mPaint = new Paint();
mPaint.setColor(color);
}
public void fire() {
animator = ValueAnimator.ofInt(1, 0);
animator.setDuration(duration);
animator.setInterpolator(new AccelerateInterpolator());
animator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animatorValue = Float.parseFloat(animation.getAnimatedValue()+"") ;
// 重点 计算每一个 火花的位置
for(Element element :elements){
element.x = (float) (element.x + Math.cos(element.direction)*element.speed*animatorValue + windSpeed*windDirection);
element.y = (float) (element.y – Math.sin(element.direction)*element.speed*animatorValue + grivaty*(1-animatorValue));
}
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
listener.onAinmationEnd();
}
});
animator.start();
}
public void draw(Canvas canvas){
mPaint.setAlpha((int) (225*animatorValue));
for(Element element :elements){
canvas.drawCircle(location.x + element.x, location.y + element.y, elemetSize, mPaint);
}
}
public void setCount(int count){
this.count = count;
}
public void setColors(int colors[]){
this.colors = colors;
}
public void setDuration(int duration){
this.duration = duration;
}
public void addAnimationEndListener(AnimationEndListener listener){
this.listener = listener;
}
private static final int[] baseColors = { 0xFFFF43, 0x00E500, 0x44CEF6, 0xFF0040, 0xFF00FFB7, 0x008CFF, 0xFF5286,
0x562CFF, 0x2C9DFF, 0x00FFFF, 0x00FF77, 0x11FF00, 0xFFB536, 0xFF4618, 0xFF334B, 0x9CFA18 };
interface AnimationEndListener {
void onAinmationEnd();
}
static class Location {
public float x;
public float y;
public Location(float x, float y) {
this.x = x;
this.y = y;
}
}

自定义view

public class FireWorkView extends View {
private final String TAG = this.getClass().getSimpleName();
private EditText mEditText;
private LinkedList fireWorks = new LinkedList ();
private int windSpeed;
public FireWorkView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void bindEditText(EditText editText) {
this.mEditText = editText;
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
float[] coordinate = getCursorCoordinate();
launch(coordinate[0], coordinate[1], before ==0?-1:1);
}
private void launch(float f, float g, int i) {
final FireWork firework = new FireWork(new FireWork.Location(f, g), i);
firework.addAnimationEndListener(new FireWork.AnimationEndListener() {
@Override
public void onAinmationEnd() {
//动画结束后把firework移除,当没有firework时不会刷新页面
fireWorks.remove(firework);
}
});
fireWorks.add(firework);
firework.fire();
invalidate();
}
private float[] getCursorCoordinate() {
/*
* 以下通过反射获取光标cursor的坐标。
* 首先观察到TextView的invalidateCursorPath()方法,它是光标闪动时重绘的方法。
* 方法的最后有个invalidate(bounds.left + horizontalPadding, bounds.top
* + verticalPadding, bounds.right + horizontalPadding,
* bounds.bottom + verticalPadding); 即光标重绘的区域,由此可得到光标的坐标
* 具体的坐标在TextView.mEditor.mCursorDrawable里,
* 获得Drawable之后用getBounds()得到Rect。 之后还要获得偏移量修正,通过以下三个方法获得:
* getVerticalOffset(),getCompoundPaddingLeft(),
* getExtendedPaddingTop()。
*
*/
int xOffset = 0;
int yOffset = 0;
Class<?> clazz = EditText.class;
clazz = clazz.getSuperclass();// 获得 TextView 这个类
try {
Field editor = clazz.getDeclaredField("mEditor");
editor.setAccessible(true);
Object mEditor = editor.get(mEditText);
Class<?> editorClazz = Class.forName("android.widget.Editor");
Field drawables = editorClazz.getDeclaredField("mCursorDrawable");
drawables.setAccessible(true);
Drawable[] drawable = (Drawable[]) drawables.get(mEditor);
Method getVerticalOffset = clazz.getDeclaredMethod("getVerticalOffset", boolean.class);
Method getCompoundPaddingLeft = clazz.getDeclaredMethod("getCompoundPaddingLeft");
Method getExtendedPaddingTop = clazz.getDeclaredMethod("getExtendedPaddingTop");
getVerticalOffset.setAccessible(true);
getCompoundPaddingLeft.setAccessible(true);
getExtendedPaddingTop.setAccessible(true);
if (drawable != null) {
Rect bounds = drawable[0].getBounds();
xOffset = Integer.parseInt(getCompoundPaddingLeft.invoke(mEditText) + "") + bounds.left;
yOffset = Integer.parseInt(getExtendedPaddingTop.invoke(mEditText) + "")
+ Integer.parseInt(getVerticalOffset.invoke(mEditText, false) + "") + bounds.bottom;
}
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
float x = mEditText.getX()+xOffset ;
float y = mEditText.getY()+yOffset ;
return new float[] { x, y };
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// TODO Auto-generated method stub
}
@Override
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
}
});
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
for (int i =0 ; i fireWorks.get(i).draw(canvas);
}
if (fireWorks.size()>0)
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}

见证奇迹的时刻

public class MainActivity extends Activity{
private EditText mEditText;
private FireWorkView mFireworkView;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEditText = (EditText) findViewById(R.id.edit_text);
mFireworkView = (FireWorkView) findViewById(R.id.fireworkview);
mFireworkView.bindEditText(mEditText);
}

到此我们烟花效果便是全部实现完毕。欢迎指正品评。最后,也是 最重要的 特别感谢 郭霖大神的技术支持。

射虎不成重练箭,斩龙不断再磨刀