摆脱枯燥的文字输入,让输入更加炫彩。 老规矩先上图。
难点
难点一:获取光标的坐标
难点二:烟花动画实现
光标坐标的计算
我们发现 api里并没有可以直接获取光标坐标的方法。api没有并不是说就没有。源码里肯定有,不然他光标是怎么画出来的呢。对吧。打开EditView的源码,只有一百多行,里面并没有关于光标的代码,那只好找他爸爸了—TextView。打开吓一跳,一万多行的代码,看源码讲究根据蛛丝马迹来推算。光标的英文是cursor。
最终我们看到了
invalidateCursorPath()->invalidateCursor()->invalidateCursor(where, where, where)->invalidateRegion(start, end,true/* Also invalidates blinking cursor */);终于找到了 这个方法invalidateRegion。
普及一下 android 字体的测量知识。
光标的测量原理也是如此。我们需要得到光标的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<Element> elements = new ArrayList<Element>(); 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<FireWork> fireWorks = new LinkedList<FireWork>(); 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.size(); 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); }到此我们烟花效果便是全部实现完毕。欢迎指正品评。最后,也是 最重要的 特别感谢 郭霖大神的技术支持。
射虎不成重练箭,斩龙不断再磨刀