1. 程式人生 > >Android EditText 新增煙花效果

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

到此我們煙花效果便是全部實現完畢。歡迎指正品評。最後,也是 最重要的 特別感謝 郭霖大神的技術支援。

射虎不成重練箭,斬龍不斷再磨刀