Android EditText 新增煙花效果
阿新 • • 發佈:2019-01-11
擺脫枯燥的文字輸入,讓輸入更加炫彩。 老規矩先上圖。
難點
難點一:獲取游標的座標
難點二:煙花動畫實現
游標座標的計算
我們發現 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);
}
到此我們煙花效果便是全部實現完畢。歡迎指正品評。最後,也是 最重要的 特別感謝 郭霖大神的技術支援。
射虎不成重練箭,斬龍不斷再磨刀