一个绚丽易用的输入框烟花效果,模仿网页360搜索框。
gif图片表现效果不好,实际的Demo里显示的效果更佳,同时不会有任何卡顿。
EditTextFirework-demo请访问我的Giehub。
https://github.com/covetcode/EditTextFirework-Demo
在使用反射寻找光标的位置时,遇到一个很大的坑,明明在EditText源码中看到的方法,偏偏用反射找不到,报错。在我百思不得其解的时候我把class的名字打印出来才发现系统调用的居然是support V7的EditText类,然而我导入的只是android.widget.EditText类。这点我完全搞不懂,谁知道的麻烦告诉我一下。
模拟烟火要关注一下几点:
-
爆炸的位置:光标所在位置。
-
火花飞出的方向:我采用随机方向,0~180度,即只向上。
-
发射速度:每个火花发射的速度是不一样的,在一定范围内随机。发射后速度衰减。
-
风:风速固定,方向根据文字的增长或减少决定。
-
重力:烟花飞出的应该是一条抛物线
-
火花的颜色:单次次发射的所有火花颜色一样,每次从颜色库随机挑选。
-
什么时候发射烟花:监听edittext,当文字改变时,获取文字数量的变化以确定风的方向。获取光标的位置确定爆炸的位置。
难点:
光标的位置。反射。没有具体的方法确定坐标,要自己计算。
基本思路确定之后我们就来用代码实现。
库里包含三个类:
Element(int color, Double direction, float speed)
烟花的小火花,存放颜色,飞行方向,飞行速度这三个变量。
Firework(Location location, int windDirection)
烟花,控制整个烟花的动画,计算小火花的位置并绘制小火花。
FireworkView()
View类,监听EditText中文字的改变,并获取光标的位置。在该位置生成Firework。
首先我们看看FireworkView的使用方法:
mFireworkView = (FireworkView) findViewById(R.id.fire_work);
mFireworkView.bindEditText(mEditText);
是不是很简单,只要绑定需要呈现烟花效果的EditText就行了。
Class FireworkView:
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
/*
i为EditText里的字符数,i1为减少的字符数,i2为增加的字符数。
关于launch的第三个参数,决定风的方向,1为吹向右边,-1为左边。
*/
float [] coordinate = getCursorCoordinate();
launch(coordinate[0], coordinate[1], i1 ==0?-1:1);
}
@Override
public void afterTextChanged(Editable editable) {
}
});
在bindEditText()中我们监听EditText。当文字有改变时,首先计算文字是增多还是减少,以确定风的方向。然后getCursorCoordinate()获得光标的坐标。最后就可以发射烟花了。
Class FireworkView:
private void launch(float x, float y, int direction){
final Firework firework = new Firework(new Firework.Location(x, y), direction);
firework.addAnimationEndListener(new Firework.AnimationEndListener() {
@Override
public void onAnimationEnd() {
//动画结束后把firework移除,当没有firework时不会刷新页面
fireworks.remove(firework);
}
});
fireworks.add(firework);
firework.fire();
invalidate();
}
用LinkedList<Firework>保存正在动画的Firework,如果里面Firework的数量不为0就不断地重绘view以实现动画,为0时不重绘。
Class Firework:
public void fire(){
animator = ValueAnimator.ofFloat(1,0);
animator.setDuration(duration);
animator.setInterpolator(new AccelerateInterpolator(2));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
animatorValue = (float) valueAnimator.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
+ gravity*(1-animatorValue));
}
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
listener.onAnimationEnd();
}
});
animator.start();
}
用一个ValueAnimator实现动画。
由于发射速度是衰减的,所以animator = ValueAnimator.ofFloat(1,0);
同时设定一个new AccelerateInterpolator(2),即加速度是增长的。
如果对Interpolator不熟悉可以看http://my.oschina.net/banxi/blog/135633 。
Class Firework:
public void draw(Canvas canvas){
mPaint.setAlpha((int) (225*animatorValue));
for (Element element : elements){
canvas.drawCircle(location.x + element.x, location.y + element.y, elementSize, mPaint);
}
}
最后只要不断地绘制小火花就行了。
如何获得光标的位置呢?
涉及到反射,需要自己查看TextView(EditText的父类是TextView)的源码并理清绘制过程。下面注释说的很清楚了,这里就不再反复说明。
Class FireworkView:
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();
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 ){
if (drawable[0] != 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();
}
float x = mEditText.getX() + xOffset;
float y = mEditText.getY() + yOffset;
return new float[]{ x , y};
}
网友评论
AccelerateInterpolator不是加速插值的吗?
你看看是不是这个问题,
compileSdkVersion
buildToolsVersion