为你的EditText添加一个烟花效果

一个绚丽易用的输入框烟花效果,模仿网页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};
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容