前段时间在写直播的时候,需要观众在看直播的时候点赞的效果,在此参照了腾讯大神写的点赞(飘心动画效果)。下面是效果图:
在attrs.xml 中增加自定义的属性
"HeartLayout">
"initX" format="dimension"/>
"initY" format="dimension"/>
"xRand" format="dimension"/>
"animLengthRand" format="dimension"/>
"xPointFactor" format="dimension"/>
"animLength" format="dimension"/>
"heart_width" format="dimension"/>
"heart_height" format="dimension"/>
"bezierFactor" format="integer"/>
"anim_duration" format="integer"/>
2.1 dimens.xml
"heart_anim_bezier_x_rand">50.0dp
"heart_anim_init_x">50.0dp
"heart_anim_init_y">25.0dp
"heart_anim_length">400.0dp
"heart_anim_length_rand">350.0dp
"heart_anim_x_point_factor">30.0dp
"heart_size_height">27.3dp
"heart_size_width">32.5dp
2.2 integers.xml
<resources>
<integer name="heart_anim_bezier_factor">6integer>
<integer name="anim_duration">3000integer>
resources>
3.1 AbstractPathAnimator.java
public abstract class AbstractPathAnimator {
private final Random mRandom;
protected final Config mConfig;
public AbstractPathAnimator(Config config) {
mConfig = config;
mRandom = new Random();
}
public float randomRotation() {
return mRandom.nextFloat() * 28.6F - 14.3F;
}
public Path createPath(AtomicInteger counter, View view, int factor) {
Random r = mRandom;
int x = r.nextInt(mConfig.xRand);
int x2 = r.nextInt(mConfig.xRand);
int y = view.getHeight() - mConfig.initY;
int y2 = counter.intValue() * 15 + mConfig.animLength * factor + r.nextInt(mConfig.animLengthRand);
factor = y2 / mConfig.bezierFactor;
x = mConfig.xPointFactor + x;
x2 = mConfig.xPointFactor + x2;
int y3 = y - y2;
y2 = y - y2 / 2;
Path p = new Path();
p.moveTo(mConfig.initX, y);
p.cubicTo(mConfig.initX, y - factor, x, y2 + factor, x, y2);
p.moveTo(x, y2);
p.cubicTo(x, y2 - factor, x2, y3 + factor, x2, y3);
return p;
}
public abstract void start(View child, ViewGroup parent);
public static class Config {
public int initX;
public int initY;
public int xRand;
public int animLengthRand;
public int bezierFactor;
public int xPointFactor;
public int animLength;
public int heartWidth;
public int heartHeight;
public int animDuration;
static public Config fromTypeArray(TypedArray typedArray, float x, float y, int pointx, int heartWidth, int heartHeight) {
Config config = new Config();
Resources res = typedArray.getResources();
config.initX = (int) typedArray.getDimension(R.styleable.HeartLayout_initX,
x);
config.initY = (int) typedArray.getDimension(R.styleable.HeartLayout_initY,
y);
config.xRand = (int) typedArray.getDimension(R.styleable.HeartLayout_xRand,
res.getDimensionPixelOffset(R.dimen.heart_anim_bezier_x_rand));
config.animLength = (int) typedArray.getDimension(R.styleable.HeartLayout_animLength,
res.getDimensionPixelOffset(R.dimen.heart_anim_length));//动画长度
config.animLengthRand = (int) typedArray.getDimension(R.styleable.HeartLayout_animLengthRand,
res.getDimensionPixelOffset(R.dimen.heart_anim_length_rand));
config.bezierFactor = typedArray.getInteger(R.styleable.HeartLayout_bezierFactor,
res.getInteger(R.integer.heart_anim_bezier_factor));
config.xPointFactor = pointx;
// config.heartWidth = (int) typedArray.getDimension(R.styleable.HeartLayout_heart_width,
// res.getDimensionPixelOffset(R.dimen.heart_size_width));//动画图片宽度
// config.heartHeight = (int) typedArray.getDimension(R.styleable.HeartLayout_heart_height,
// res.getDimensionPixelOffset(R.dimen.heart_size_height));//动画图片高度
config.heartWidth = heartWidth;
config.heartHeight = heartHeight;
config.animDuration = typedArray.getInteger(R.styleable.HeartLayout_anim_duration,
res.getInteger(R.integer.anim_duration));//持续期
return config;
}
}
}
3.2 PathAnimator.java
/**
* 飘心路径动画器
*/
public class PathAnimator extends AbstractPathAnimator {
private final AtomicInteger mCounter = new AtomicInteger(0);
private Handler mHandler;
public PathAnimator(Config config) {
super(config);
mHandler = new Handler(Looper.getMainLooper());
}
@Override
public void start(final View child, final ViewGroup parent) {
parent.addView(child, new ViewGroup.LayoutParams(mConfig.heartWidth, mConfig.heartHeight));
FloatAnimation anim = new FloatAnimation(createPath(mCounter, parent, 2), randomRotation(), parent, child);
anim.setDuration(mConfig.animDuration);
anim.setInterpolator(new LinearInterpolator());
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
mHandler.post(new Runnable() {
@Override
public void run() {
parent.removeView(child);
}
});
mCounter.decrementAndGet();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationStart(Animation animation) {
mCounter.incrementAndGet();
}
});
anim.setInterpolator(new LinearInterpolator());
child.startAnimation(anim);
}
static class FloatAnimation extends Animation {
private PathMeasure mPm;
private View mView;
private float mDistance;
private float mRotation;
public FloatAnimation(Path path, float rotation, View parent, View child) {
mPm = new PathMeasure(path, false);
mDistance = mPm.getLength();
mView = child;
mRotation = rotation;
parent.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
@Override
protected void applyTransformation(float factor, Transformation transformation) {
Matrix matrix = transformation.getMatrix();
mPm.getMatrix(mDistance * factor, matrix, PathMeasure.POSITION_MATRIX_FLAG);
mView.setRotation(mRotation * factor);
float scale = 1F;
if (3000.0F * factor < 200.0F) {
scale = scale(factor, 0.0D, 0.06666667014360428D, 0.20000000298023224D, 1.100000023841858D);
} else if (3000.0F * factor < 300.0F) {
scale = scale(factor, 0.06666667014360428D, 0.10000000149011612D, 1.100000023841858D, 1.0D);
}
mView.setScaleX(scale);
mView.setScaleY(scale);
transformation.setAlpha(1.0F - factor);
}
}
private static float scale(double a, double b, double c, double d, double e) {
return (float) ((a - b) / (c - b) * (e - d) + d);
}
}
4.1 HeartView.java
/**
* 飘心动画的界面
*/
public class HeartView extends ImageView{
//绘制的时候抗锯齿
private static final Paint sPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private static final Canvas sCanvas = new Canvas();
private int mHeartResId = R.drawable.heart0;
private int mHeartBorderResId = R.drawable.heart1;
private static Bitmap sHeart;
private static Bitmap sHeartBorder;
public HeartView(Context context) {
super(context);
}
public HeartView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HeartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setDrawable(int resourceId){
Bitmap heart = BitmapFactory.decodeResource(getResources(), resourceId);
// Sets a drawable as the content of this ImageView.
setImageDrawable(new BitmapDrawable(getResources(),heart));
}
public void setColor(int color) {
Bitmap heart = createHeart(color);
setImageDrawable(new BitmapDrawable(getResources(), heart));
}
public void setColorAndDrawables(int color, int heartResId, int heartBorderResId) {
if (heartResId != mHeartResId) {
sHeart = null;
}
if (heartBorderResId != mHeartBorderResId) {
sHeartBorder = null;
}
mHeartResId = heartResId;
mHeartBorderResId = heartBorderResId;
setColor(color);
}
public Bitmap createHeart(int color) {
if (sHeart == null) {
sHeart = BitmapFactory.decodeResource(getResources(), mHeartResId);
}
if (sHeartBorder == null) {
sHeartBorder = BitmapFactory.decodeResource(getResources(), mHeartBorderResId);
}
Bitmap heart = sHeart;
Bitmap heartBorder = sHeartBorder;
Bitmap bm = createBitmapSafely(heartBorder.getWidth(), heartBorder.getHeight());
if (bm == null) {
return null;
}
Canvas canvas = sCanvas;
canvas.setBitmap(bm);
Paint p = sPaint;
canvas.drawBitmap(heartBorder, 0, 0, p);
p.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP));
float dx = (heartBorder.getWidth() - heart.getWidth()) / 2f;
float dy = (heartBorder.getHeight() - heart.getHeight()) / 2f;
canvas.drawBitmap(heart, dx, dy, p);
p.setColorFilter(null);
canvas.setBitmap(null);
return bm;
}
private static Bitmap createBitmapSafely(int width, int height) {
try {
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
} catch (OutOfMemoryError error) {
error.printStackTrace();
}
return null;
}
}
4.2 飘心动画路径布局
HeartLayout.java
/**
* 飘心动画路径
*/
public class HeartLayout extends RelativeLayout implements View.OnClickListener {
private AbstractPathAnimator mAnimator;
private AttributeSet attrs = null;
private int defStyleAttr = 0;
private OnHearLayoutListener onHearLayoutListener;
private static HeartHandler heartHandler;
private static HeartThread heartThread;
public void setOnHearLayoutListener(OnHearLayoutListener onHearLayoutListener) {
this.onHearLayoutListener = onHearLayoutListener;
}
public interface OnHearLayoutListener {
boolean onAddFavor();
}
public HeartLayout(Context context) {
super(context);
findViewById(context);
}
public HeartLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.attrs = attrs;
findViewById(context);
}
public HeartLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.attrs = attrs;
this.defStyleAttr = defStyleAttr;
findViewById(context);
}
private Bitmap bitmap;
private void findViewById(Context context) {
LayoutInflater.from(context).inflate(R.layout.ly_periscope, this);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_like);
dHeight = bitmap.getWidth()/2;
dWidth = bitmap.getHeight()/2;
textHight = sp2px(getContext(), 20) + dHeight / 2;
pointx = dWidth;//随机上浮方向的x坐标
bitmap.recycle();
}
private int mHeight;
private int mWidth;
private int textHight;
private int dHeight;
private int dWidth;
private int initX;
private int pointx;
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
public class HeartHandler extends Handler {
public final static int MSG_SHOW = 1;
WeakReference wf;
public HeartHandler(HeartLayout layout) {
wf = new WeakReference(layout);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
HeartLayout layout = wf.get();
if (layout == null) return;
switch (msg.what) {
case MSG_SHOW:
addFavor();
break;
}
}
}
public class HeartThread implements Runnable {
private long time = 0;
private int allSize = 0;
public void addTask(long time, int size) {
this.time = time;
allSize += size;
}
public void clean() {
allSize = 0;
}
@Override
public void run() {
if (heartHandler == null) return;
if (allSize > 0) {
heartHandler.sendEmptyMessage(HeartHandler.MSG_SHOW);
allSize--;
}
postDelayed(this, time);
}
}
private void init(AttributeSet attrs, int defStyleAttr) {
final TypedArray a = getContext().obtainStyledAttributes(
attrs, R.styleable.HeartLayout, defStyleAttr, 0);
if (pointx <= initX && pointx >= 0) {
pointx -= 10;
} else if (pointx >= -initX && pointx <= 0) {
pointx += 10;
} else pointx = initX;
mAnimator = new PathAnimator(AbstractPathAnimator.Config.fromTypeArray(a, initX, textHight, pointx, dWidth, dHeight));
a.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取本身的宽高 这里要注意,测量之后才有宽高
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
initX = mWidth / 2 - dWidth / 2;
}
public AbstractPathAnimator getAnimator() {
return mAnimator;
}
public void setAnimator(AbstractPathAnimator animator) {
clearAnimation();
mAnimator = animator;
}
public void clearAnimation() {
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).clearAnimation();
}
removeAllViews();
}
private static int[] drawableIds = new int[]{R.drawable.heart0, R.drawable.heart1, R.drawable.heart2, R.drawable.heart3, R.drawable.heart4, R.drawable.heart5, R.drawable.heart6, R.drawable.heart7, R.drawable.heart8,};
private Random random = new Random();
public void addFavor() {
HeartView heartView = new HeartView(getContext());
heartView.setDrawable(drawableIds[random.nextInt(8)]);
init(attrs, defStyleAttr);
mAnimator.start(heartView, this);
}
private long nowTime, lastTime;
final static int[] sizeTable = {9, 99, 999, 9999, 99999, 999999, 9999999,
99999999, 999999999, Integer.MAX_VALUE};
public static int sizeOfInt(int x) {
for (int i = 0; ; i++)
if (x <= sizeTable[i])
return i + 1;
}
public void addFavor(int size) {
switch (sizeOfInt(size)) {
case 1:
size = size % 10;
break;
default:
size = size % 100;
}
if (size == 0) return;
nowTime = System.currentTimeMillis();
long time = nowTime - lastTime;
if (lastTime == 0)
time = 2 * 1000;//第一次分为2秒显示完
time = time / (size + 15);
if (heartThread == null) {
heartThread = new HeartThread();
}
if (heartHandler == null) {
heartHandler = new HeartHandler(this);
heartHandler.post(heartThread);
}
heartThread.addTask(time, size);
lastTime = nowTime;
}
public void addHeart(int color) {
HeartView heartView = new HeartView(getContext());
heartView.setColor(color);
init(attrs, defStyleAttr);
mAnimator.start(heartView, this);
}
public void addHeart(int color, int heartResId, int heartBorderResId) {
HeartView heartView = new HeartView(getContext());
heartView.setColorAndDrawables(color, heartResId, heartBorderResId);
init(attrs, defStyleAttr);
mAnimator.start(heartView, this);
}
@Override
public void onClick(View v) {
int i = v.getId();
if (i == R.id.img) {
if (onHearLayoutListener != null) {
boolean isAdd = onHearLayoutListener.onAddFavor();
if (isAdd) addFavor();
}
}
}
public void clean() {
if (heartThread != null) {
heartThread.clean();
}
}
public void release() {
if (heartHandler != null) {
heartHandler.removeCallbacks(heartThread);
heartThread = null;
heartHandler = null;
}
}
}
5.1 activity_heart_animal.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/grey"
android:alpha="0.5">
<TextView
android:id="@+id/member_send_good"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginRight="30dp"
android:layout_marginBottom="10dp"
android:background="@drawable/live_like_icon"
/>
<com.myapplication2.app.newsdemo.view.heartview.HeartLayout
android:id="@+id/heart_layout"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"/>
RelativeLayout>
5.2 activity 中的使用
heartLayout = (HeartLayout)findViewById(R.id.heart_layout);
findViewById(R.id.member_send_good).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
heartLayout.addFavor();
}
});
heartLayout.addFavor(); 就是触发飘心动画效果的关键代码
https://github.com/zhaoyang21cn/Android_Suixinbo