关键点:
爆炸的位置:光标所在位置。
火花飞出的方向:我采用随机方向,0~180度,即只向上。
发射速度:每个火花发射的速度是不一样的,在一定范围内随机。发射后速度衰减。
风:风速固定,方向根据文字的增长或减少决定。
重力:烟花飞出的应该是一条抛物线。
火花的颜色:单次次发射的所有火花颜色一样,每次从颜色库随机挑选。
什么时候发射烟花:监听edittext,当文字改变时,获取文字数量的变化以确定风的方向。获取光标的位置确定爆炸的位置。
难点:光标的位置;反射;没有具体的方法确定坐标,要自己计算。
布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:id="@+id/layout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" tools:context="com.example.demoeiter.MainActivity">
<TextView
android:layout_marginTop="100dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#6e6e6e"
android:gravity="center"
android:text="EditText-Firework"/>
<EditText
android:layout_marginTop="150dp"
android:layout_centerHorizontal="true"
android:textColor="#6e6e6e"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:id="@+id/edit_text"/>
<com.example.demoeiter.FireworkView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fire_work"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="day"
android:padding="10dp"
android:textColor="#FFFFFF"
android:id="@+id/day"
android:layout_alignTop="@+id/night"
android:layout_alignRight="@+id/edit_text"
android:layout_alignEnd="@+id/edit_text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="night"
android:textColor="#000000"
android:id="@+id/night"
android:layout_marginBottom="40dp"
android:layout_alignParentBottom="true"
android:layout_alignLeft="@+id/edit_text"
android:layout_alignStart="@+id/edit_text" />
</RelativeLayout>
Element
package com.example.demoeiter;
/**烟花的小火花,存放颜色,飞行方向,飞行速度这三个变量。
* Created by 李倩 on 2017/12/8.
*/
public class Element {
public int color;
public Double direction;
public float speed;
public float x = 0;
public float y = 0;
//Element(int color, Double direction, float speed)
// 烟花的小火花,存放颜色,飞行方向,飞行速度这三个变量。
public Element(int color, Double direction, float speed){
this.color = color;
this.direction = direction;
this.speed = speed;
}
}
Firework
package com.example.demoeiter;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.animation.AccelerateInterpolator;
import java.util.ArrayList;
import java.util.Random;
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 ; //count of element
private int duration;
private int[] colors;
private int color;
private float launchSpeed;
private float windSpeed;
private float gravity;
private int windDirection; //1 or -1
private Location location;
private float elementSize;
private ValueAnimator animator;
private float animatorValue;
private ArrayList<Element> elements = new ArrayList<>();
private AnimationEndListener listener;
/**
* Firework(Location location, int windDirection)
烟花,控制整个烟花的动画,计算小火花的位置并绘制小火花。
* @param location
* @param windDirection
*/
public Firework(Location location, int windDirection){
this.location = location;
this.windDirection = windDirection;
colors = baseColors;
duration = DEFAULT_DURATION;
gravity = DEFAULT_GRAVITY;
elementSize = 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);
// BlurMaskFilter maskFilter = new BlurMaskFilter(2, BlurMaskFilter.Blur.NORMAL);
// mPaint.setMaskFilter(maskFilter);
}
/**
* 用一个 ValueAnimator 实现动画。
* 由于发射速度是衰减的,
* 所以需要设定一个 new AccelerateInterpolator(2)。
*/
public void fire(){
animator = ValueAnimator.ofFloat(1,0);
animator.setDuration(duration);
animator.setInterpolator(new AccelerateInterpolator());
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();
}
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;
}
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);
}
}
private static final int[] baseColors = {0xFFFF43,0x00E500,0x44CEF6,0xFF0040,0xFF00FFB7,0x008CFF
,0xFF5286,0x562CFF,0x2C9DFF,0x00FFFF,0x00FF77,0x11FF00,0xFFB536,0xFF4618,0xFF334B,0x9CFA18};
interface AnimationEndListener{
void onAnimationEnd();
}
static class Location{
public float x;
public float y;
public Location(float x, float y){
this.x = x;
this.y = y;
}
}
}
FireworkView
package com.example.demoeiter;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;
/**FireworkView()
View类,监听 EditText 中文字的改变,并获取光标的位置。在该位置生成 Firework。
* Created by wayww on 2016/9/8.
*/
public class FireworkView extends View {
private final String TAG = this.getClass().getSimpleName();
private EditText mEditText;
private LinkedList<Firework> fireworks = new LinkedList<>();
private int windSpeed;
private TextWatcher mTextWatcher;
public FireworkView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public FireworkView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FireworkView(Context context) {
super(context);
}
// 在 bindEditText() 中我们监听 EditText。当文字有改变时,首先计算文字是增多还是减少,
//以确定风的方向。然后 getCursorCoordinate() 获得光标的坐标。最后就可以发射烟花了
public void bindEditText(EditText editText) {
this.mEditText = editText;
mEditText.addTextChangedListener( mTextWatcher = new TextWatcher() {
@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) {
}
});
}
public void removeBind(){
mEditText.removeTextChangedListener(mTextWatcher);
mEditText = null;
}
//~~~~~~~~~~~~~private method~~~~~~~~~~~~~~~~~~~
/**
* 用 LinkedList<Firework> 保存正在动画的 Firework,如果里面 Firework
* 的数量不为0就不断地 重绘view 以实现动画,为0时不重绘。
* @param x
* @param y
* @param direction
*/
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();
}
/**获得光标
* @return the coordinate of cursor. x=float[0]; y=float[1];
*/
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){
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;
//当EditText的父view与FireworkView的坐标(左上角的坐标值)不一致时进行修正
int[] positionE = new int[2];
if (mEditText.getParent() != null) {
((ViewGroup)mEditText.getParent()).getLocationInWindow(positionE);
}
int[] positionF = new int[2];
this.getLocationInWindow(positionF);
x = x+positionE[0]-positionF[0];
y = y+positionE[1]-positionF[1];
return new float[]{x , y};
}
@Override
protected void onDraw(Canvas canvas) {
for (int i =0 ; i<fireworks.size(); i++){
fireworks.get(i).draw(canvas);
}
if (fireworks.size()>0)
invalidate();
}
}
MainActivity
package com.example.demoeiter;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private EditText mEditText;
private FireworkView mFireworkView;
private RelativeLayout layout;
private TextView day;
private TextView night;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
layout = (RelativeLayout) findViewById(R.id.layout);
day = (TextView) findViewById(R.id.day);
day.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layout.setBackgroundColor(0xFFFFFFFF);
}
});
night = (TextView) findViewById(R.id.night);
night.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layout.setBackgroundColor(0xFF000000);
}
});
mEditText = (EditText) findViewById(R.id.edit_text);
mFireworkView = (FireworkView) findViewById(R.id.fire_work);
mFireworkView.bindEditText(mEditText);
}
}