Android EditText 添加烟花效果

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

应用宝动态截屏2016102001.gif

难点

难点一:获取光标的坐标

难点二:烟花动画实现

光标坐标的计算

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

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);
    }

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

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

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

推荐阅读更多精彩内容