公司做视频直播的礼物动效。
前期调研的过程中发现很多竞品竟然都是利用帧动画做的。
利用帧动画当然不能直接加载多张图片,要知道最大的礼物有一百多张图片,有OOM的风险。
所以利用SurfaceView实现了帧动画。这样可以控制内存一直处于非常底的范围内抖动。所占的CPU也比较小。
另外一种实现方案就是利用webp,直接播放webp.
webp相较与SurfaceView的帧动画优势就是内存占用更小,但是CPU占比会稍微大一点。
下面就是利用SurfaceView实现的帧动画:
public class SpecialGiftSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private static final String TAG = "Surface";
private static final long INTERVAL_TIME = 66;//最大间隔时间,每帧时间为最大时间减去加载图片消耗的时间。
private SurfaceHolder mHolder;
private boolean isDrawing = false;
private boolean isSurfaceCreated = false;
private List<String> mFilePathListRGB = new ArrayList<>();
private List<String> mFilePathListAlpha = new ArrayList<>();
private HandlerThread handlerThread = new HandlerThread("surfaceview");
private RectF mRectF;
private OnFrameAnimationListener mListener;
private Handler mWorkHandler;
public SpecialGiftSurfaceView(Context context) {
super(context);
init();
}
public SpecialGiftSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mHolder = getHolder();
mHolder.addCallback(this);
//设置SurfaceView透明
setZOrderOnTop(true);
mHolder.setFormat(PixelFormat.TRANSLUCENT);
handlerThread.start();
}
/**
* 开始帧动画
* @param pathListRGB 彩色图片路径
* @param pathListAlpha 透明图片路径
*/
public void startAnimation(List<String> pathListRGB, List<String> pathListAlpha) {
long delay = 0;
if (pathListRGB == null || pathListRGB.size() == 0) {
return;
}
setVisibility(VISIBLE);
mFilePathListRGB.clear();
mFilePathListRGB.addAll(pathListRGB);
mFilePathListAlpha.clear();
if (pathListAlpha != null && mFilePathListAlpha.size() > 0) {
mFilePathListAlpha.addAll(pathListAlpha);
}
mWorkHandler = new Handler(handlerThread.getLooper());
if (!isSurfaceCreated) {
Log.d(TAG, "SurfaceView is not created.wait 1000");
delay = 1000;
}
setLayerType(LAYER_TYPE_HARDWARE, null);
mWorkHandler.postDelayed(this, delay);
}
public void startAnimation(List<String> pathListRGB) {
startAnimation(pathListRGB, null);
}
/**
* 停止动画
*/
public void stopAnimation() {
mFilePathListRGB.clear();
mFilePathListAlpha.clear();
setVisibility(INVISIBLE);
setLayerType(LAYER_TYPE_NONE, null);
isDrawing = false;
if (mWorkHandler != null) {
mWorkHandler.removeCallbacks(this);
}
}
public void setListener(OnFrameAnimationListener listener) {
mListener = listener;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
isSurfaceCreated = true;
isDrawing = true;
Log.d(TAG, "surfaceCreated");
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
stopAnimation();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if(keyCode == event.KEYCODE_BACK) {
stopAnimation();
}
return super.onKeyDown(keyCode, event);
}
@Override
public void run() {
SpecialGiftSurfaceView.this.post(new Runnable() {
@Override
public void run() {
notifyStart();
}
});
for (int i = 0; i < mFilePathListRGB.size() ; i++) {
if (isDrawing) {
try {
long temp = System.currentTimeMillis();
if (mFilePathListAlpha != null && mFilePathListAlpha.size() > 0) {
draw(mFilePathListRGB.get(i), mFilePathListAlpha.get(i));
} else {
draw(mFilePathListRGB.get(i));
}
//间隔幅度越小,CPU占比越大。所以应该合理设置。
long ll = System.currentTimeMillis() - temp;
Log.d(TAG, "id :" + i + " temp :" + ll);
Thread.sleep(Math.max(0, (INTERVAL_TIME- ll)));
} catch (Exception e) {
e.printStackTrace();
}
} else {
break;
}
}
SpecialGiftSurfaceView.this.post(new Runnable() {
@Override
public void run() {
stopAnimation();
notifyFinished();
}
});
}
private void draw(String path) {
Canvas canvas = mHolder.lockCanvas();
if (canvas != null) {
Bitmap diskBitmap = getDiskBitmap(path);
if (diskBitmap != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mRectF = new RectF(SpecialGiftSurfaceView.this.getLeft(),
SpecialGiftSurfaceView.this.getTop(),
SpecialGiftSurfaceView.this.getWidth(),
SpecialGiftSurfaceView.this.getHeight());
canvas.drawBitmap(diskBitmap, null, mRectF, null);
}
mHolder.unlockCanvasAndPost(canvas);
}
}
private void draw(String pathRGB, String pathAlpha) {
Canvas canvas = mHolder.lockCanvas();
if (canvas != null) {
int saveCount = canvas.getSaveCount();
Paint l = new Paint();
l.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
Paint m = new Paint();
m.setColorFilter(new ColorMatrixColorFilter(new ColorMatrix(new float[]{
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, 0.0f, 0.0f})));
Bitmap decodeFile = getDiskBitmap(pathRGB);
Bitmap decodeFile2 = getDiskBitmap(pathAlpha);
if (!(decodeFile == null || decodeFile2 == null)) {
Bitmap r = Bitmap.createBitmap(decodeFile.getWidth(), decodeFile.getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(r);
RectF rectF = new RectF(SpecialGiftSurfaceView.this.getLeft(),
SpecialGiftSurfaceView.this.getTop(),
SpecialGiftSurfaceView.this.getWidth(),
SpecialGiftSurfaceView.this.getHeight());
c.drawBitmap(decodeFile2, 0.0f, 0.0f, m);
c.drawBitmap(decodeFile, 0.0f, 0.0f, l);
canvas.drawBitmap(r, null, rectF, null);
canvas.restoreToCount(saveCount);
}
mHolder.unlockCanvasAndPost(canvas);
}
}
private Bitmap getDiskBitmap(String pathString) {
Bitmap bitmap = null;
try {
File file = new File(pathString);
if (file.exists()) {
bitmap = BitmapFactory.decodeFile(pathString);
}
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
private void notifyStart() {
if (mListener != null) {
mListener.onFrameAnimationStart();
}
}
private void notifyFinished() {
if (mListener != null) {
mListener.onFrameAnimationFinished();
}
}
public interface OnFrameAnimationListener {
void onFrameAnimationStart();
void onFrameAnimationFinished();
}
其对外的接口有两个:
startAnimation(List pathListRGB)
startAnimation(List pathListRGB, List pathListAlpha)
这里提供两个重载的方法是有不同的含义的。
前提:
动画一定要是透明的,因为在播放礼物的同时,也一定要能看到主播的直播画面。
而且我们都知道PNG是支持透明的,而JPG是不支持透明的。
一个参数的方法,需要出入一组PNG图片的地址。
两个参数的方法,需要传入两组JPG图片的地址,第一组是正常的图片,带白色底。第二组是其蒙版,利用第二组将最后绘画出来的图片变为透明。
(这个方法是拆分猎豹的APK发现的,这样做的好处就是,JPG占的内存回避PNG的少,毕竟有很多礼物,需要的图片太多,能小则小。)