本文将以下雪为例,介绍一种Android上实现动态背景的方式。动态背景是在单独的线程中绘制,因此不会影响UI主线程。即使主线程包含动画,或者要迅速响应用户的滑动、拖拽等,都不会占用任何绘制时间。
(1)效果图
“一图抵千言”,先来看看效果动图:
上图是下雪背景与ListView的结合展示,动态的雪花与ListView的滑动互不影响。ListView可以替换为任意的View、ViewGroup。
为防止图被吞,这里是一个备份链接:https://pan.baidu.com/s/1LiHTyAnwwz4ooaKMtLArGg?pwd=u8h8
(2)主要思想
这种动态的背景,我不想在UI线程中绘制。一般来说,要让UI的帧率达到60fps,那么每一帧的绘制时间不超过16.67ms。在UI线程中绘制这样的动态背景,会严重影响性能。在那些有动画、频繁交互的场景,更会雪上加霜。
于是,想着能不能在线程中绘制?在Android中,要在独立的线程中绘制UI,只有两种办法。一种是使用SurfaceView,另一种是使用TextureView。SurfaceView拥有独立的绘图表面,和其他View是不能随意组合到一起的,因而被排除。TextureView则与父布局共用同一个绘图表面,因此可以和任意View结合,满足了我的需要。
(3) TextureView简介
TextureView的官方介绍并不多,原文如下:
A TextureView can be used to display a content stream, such as that coming from a camera preview, a video, or an OpenGL scene. The content stream can come from the application's process as well as a remote process.
TextureView can only be used in a hardware accelerated window. When rendered in software, TextureView will draw nothing.
意思是:
TextureView可以用来展示内容流,如来自相机摄像头的取景、视频或OpenGL场景。内容流可以来自应用进程,也可以来自远端进程。
TextureView只有在硬件加速开启的窗口中才能使用。如果未开启,那么TextureView什么都不绘制。
除此之外,再无过多介绍。开始有一些纳闷,从这些介绍来看,TextureView似乎是为了相机、视频等设计的,能满足我的需要吗?而且还有硬件加速的限制,这不是有很大的风险吗?Android手机的品牌和种类可谓是汗牛充栋,不胜枚举,如果用户手机不支持硬件加速,那不是白瞎吗?
带着这些疑问,做了一些深入的了解和尝试。首先硬件加速问题,在Android 3.0就支持了硬件加速,Android 4.0默认开启了硬件加速。如下:
android:hardwareAccelerated="true"
现在已经Android 13了,经过了这么多年的更新换代,市场上绝大部分的手机应该都支持了。从2021-11-23日Google发布的设备份额报告中得知,Android 4.0已经是最低系统版本,占比仅为0.4%。所以硬件加速应该不是任何阻碍了。
然后,对是否支持这种动态绘制做了进一步的尝试,发现完全没问题,可以满足需要。下面先来介绍实现思路,再介绍具体的类。
(4)基本实现思路
首先,如何产生这些雪花,它们的位置如何确定?
所有的雪花都源于同一张png图片,不同的雪花大小,是对原图进行了不同程度的缩放。它们的初始位置和结束位置,可以根据需要来设定,全屏或部分区域都行。雪花的位置在特定的范围内随机设置。比如初始位置x在[0,1440]内随机,结束位置在[x-200,x+200]区域随机。
其次,雪花的运动轨迹是怎样的?如何来更新?
雪花的运动轨迹是线性的,从随机的起始位置,运动到相应的结束位置。当然,这并不是强制的,现实生活中,雪花的飘落还会受到风力的影响,如果能以某种公式来计算各个时间点的位置,那自然更好。但这更多的是物理、数学里的问题,从实现上来讲,和线性的绘制并无区别。
雪花的下落有快有慢,这和它们的初始随机大小有关。大的雪花下落快,小的雪花下落慢,这是通过赋予它们不同的初速度来实现的。雪花的更新,是和整体运动时间有关。每间隔一小段时间,就更新各雪花的位置,并绘制到画布上。
最后,雪花的落地有一种融入的效果,如何来体现?
在雪花已经下落80%的距离后,剩下的20%再加一个渐出动画。也即是改变它的alpha值,使得落到终点时alpha=0,刚好看不见。
(5)雪花类SnowFlake
先来看看构造器:
public SnowFlake(Context context) {
this.context = context;
int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
int screenHeight = context.getResources().getDisplayMetrics().heightPixels;
alpha = (float) Math.floor(Math.random() * 8 + 2) / 10; //随机alpha值,取0.2~1之间
scale = (float) Math.floor(Math.random() * 5 + 6) / 10; //随机scale值,取0.6~1之间
startX = dp2px(5) + (int) (Math.random() * (screenWidth - dp2px(10)));
startY = -dp2px(20);
offsetX = (int) (Math.random() * dp2px(100)) - dp2px(50);
offsetY = (int) (screenHeight * 0.7f) + (int) (Math.random() * dp2px(150));
if (drawable == null){
drawable = context.getResources().getDrawable(R.drawable.snow);
}
int drawableWidth = (int) (drawable.getIntrinsicWidth() * scale);
int drawableHeight = (int) (drawable.getIntrinsicHeight() * scale);
drawable.setBounds(0, 0, drawableWidth, drawableHeight);
Bitmap bitmap = Bitmap.createBitmap(drawableWidth, drawableHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.draw(canvas);
snowFlakeBmp = bitmap;
x = startX;
y = startY;
}
雪花对应的drawable是static的,被所有的雪花对象共用。不同的雪花在构造时,透明度、大小都在一定范围内随机。初始位置及偏移也是如此。
有些变量暂时不知道定义并不要紧,后面给出本示例的Github地址,感兴趣的朋友可以去下载。
雪花的初始化,根据alpha设置不同的速度和落点:
private void init() {
if (alpha < 0.8) {
if (alpha < 0.5) {
speed = 2 * speed;
endX = startX + offsetX;
endY = offsetY;
} else {
speed = (int) (1.5f * speed);
endX = startX + offsetX;
endY = offsetY;
}
} else {
endX = startX + offsetX;
endY = offsetY;
if (scope == BIG) {
endX = startX + offsetX + (int) (endY * Math.tan(15 * Math.PI / 180));
}
}
}
判断雪花是否触底:
/**
* 当前雪花是否触底
*
* @return
*/
private void checkReachBottom() {
if (y >= (int) (endY * 0.8f)) {
isReachBottom = true;
}
}
判断雪花是否应该死亡,即最终消失:
private void checkDead() {
if (y >= endY) {
isDead = true;
}
}
更新将要触底的雪花透明度alpha:
private void updateBottomAlpha() {
int tmpY = (int) (endY * 0.2f);
int disY = y - (int) (endY * 0.8f);
float ratio = ((float) disY) / tmpY;
alpha = alpha - alpha * ratio;
}
根据时间间隔,更新雪花位置:
public void updatePos(long deltaTime) {
if (deltaTime <= 0) {
return;
}
if (isDead) {
return;
}
int factor = 45;
if (isToolbar) {
factor = 60;
}
double deltaY = ((double) (deltaTime * speed)) / (double) factor;
double deltaX = deltaY * (endX - startX) / (double) endY;
y += (int) deltaY;
if (y > 0) {
x = startX + (int) (y * (endX - startX) / (double) endY);
}
checkReachBottom();
checkDead();
if (isReachBottom) {
updateBottomAlpha();
}
}
雪花的绘制,要考虑alpha的渐变:
public void draw(Canvas canvas) {
if (isDead) {
return;
}
if (snowFlakeBmp != null) {
Paint paint = new Paint();
paint.setAlpha((int) (255 * alpha * parentAlpha));
canvas.drawBitmap(snowFlakeBmp, x, y, paint);
}
}
(6)雪花工厂类SnowFactory
上面的SnowFlake代表着单个雪花对象,本小节的SnowFactory是对众多雪花对象进行管理。
先来看看构造器:
public SnowFactory(Context context) {
this.context = context;
lockObject = new Object();
perroid = SnowFlake.getPeroid(scope);
snowFlakes = new ArrayList<>();
timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
addSnowFlake();
num++;
}
};
timer.schedule(timerTask, 1000, perroid);
}
创建了一个定时器,每隔1s就新增一个雪花。这个定时器是另外一个线程,负责触发雪花的生产,和绘制所在线程不同。雪花有新增,有更新,有绘制,有消亡,它们的处理并不在同一个线程中,所以用到了lockObject来处理同步。
生产雪花:
private void addSnowFlake() {
Log.d(TAG, "addSnowFlake() -->> size = " + snowFlakes.size());
if (snowFlakes.size() > SNOW_NUM) {
return;
}
SnowFlake snowFlake = new SnowFlake(context, isToolbar);
snowFlake.setScope(scope);
synchronized (lockObject) {
snowFlakes.add(snowFlake);
}
}
检查已消失的雪花:
private void checkDead() {
if (snowFlakes.size() > 0) {
synchronized (lockObject) {
for (int i = snowFlakes.size() - 1; i >= 0; i--) {
if (snowFlakes.get(i).isDead()) {
snowFlakes.remove(i);
}
}
}
}
}
更新所有雪花位置:
public void updatePos(long delayTime) {
Log.d(TAG, "SnowFactory updatePos() -->> delayTime = " + delayTime);
checkDead();
synchronized (lockObject) {
for (SnowFlake snowFlake : snowFlakes) {
snowFlake.updatePos(delayTime);
snowFlake.setAlpha(alpha);
}
}
}
绘制所有雪花:
public void draw(Canvas canvas) {
synchronized (lockObject) {
for (SnowFlake snowFlake : snowFlakes) {
snowFlake.draw(canvas);
}
}
}
(7)雪花绘制线程SnowDrawThread
上面提到,雪花的绘制是在单独的线程中,和UI线程不同。本小节就来介绍一下SnowDrawThread。先看看构造器:
public class SnowDrawThread extends Thread {
public SnowDrawThread(SnowFactory factory, TextureView textureView) {
setRunning(true);
this.factory = factory;
this.textureView = textureView;
}
}
很简单,传入SnowFactory和TextureView对象。再看看run()方法:
@Override
public void run() {
long deltaTime = 0;
long tickTime = System.currentTimeMillis();
while (isRunning()) {
try {
synchronized (textureView) {
canvas = textureView.lockCanvas();
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
factory.updatePos(DRAW_INTERVAL);
factory.draw(canvas);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (textureView != null && canvas != null) {
textureView.unlockCanvasAndPost(canvas);
}
}
deltaTime = System.currentTimeMillis() - tickTime;
if (deltaTime < DRAW_INTERVAL) {
try {
Thread.sleep(DRAW_INTERVAL - deltaTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
tickTime = System.currentTimeMillis();
}
try {
synchronized (textureView) {
canvas = textureView.lockCanvas();
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (textureView != null && canvas != null) {
textureView.unlockCanvasAndPost(canvas);
}
}
}
首先,通过canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)这行代码将画布清屏,防止受到上一帧的影响;然后通过factory来更新雪花位置并绘制,再通过textureView.unlockCanvasAndPost(canvas)提交绘制结果。绘制完成后,将当前线程投入睡眠。睡眠特定时间后,先清屏,再接着下一帧的绘制,如此重复。
(8)调用方
SnowDrawThread是在TextureView的相关回调中调用,而TextureView是在Activity中使用。先从布局文件看起:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@color/black"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
<ListView
android:id="@+id/listView"
android:divider="@color/white"
android:dividerHeight="1dp"
android:layout_width="match_parent"
android:layout_height="match_parent"></ListView>
</FrameLayout>
id为root_view的FrameLayout就是TextureView的父布局。Activity中的初始化:
snowFactory = new SnowFactory(this);
snowTextureView = new TextureView(this);
snowTextureView.setOpaque(false);
snowTextureView.setSurfaceTextureListener(mListener);
FrameLayout rootView = (FrameLayout) findViewById(R.id.root_view);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT);
rootView.addView(snowTextureView, layoutParams);
mListener的初始化:
TextureView.SurfaceTextureListener mListener = new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
isAvailable.set(true);
Log.d(TAG, "onSurfaceTextureAvailable() -->> ");
snowDrawThread = new SnowDrawThread(snowFactory, snowTextureView);
snowDrawThread.start();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
isAvailable.set(false);
snowDrawThread.stopThread();
snowFactory.clear();
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
};
SnowDrawThread的启动和终止就在该mListener的对应回调里。
至此,基本内容和主要实现就介绍完了。
(9)扩展
这种实现思路,可以扩展到很多方面。一些基本的平移、旋转、缩放、Alpha渐变等动画,都可以通过它来实现。特别当有循环动画时,可以减轻主线程的性能压力。只要有确定的公式,可以根据它来计算不同时间点的位置,都能运用本思路。
近些年比较流行的Lottie动画库,让Android的动画有了巨大的飞跃。但它仍然是在主线程中绘制的,这在某些性能要求高的场景很受限制。如果能深入研究一下源码,将它与本示例中的思路结合起来,用单独的线程来绘制,那可能又会是另一个飞跃。
(10)遗憾
因为时间和精力的关系,本示例并没有做到极致。有一些遗憾:
其一是雪花的下落理论上要符合重力的规律,这注定不能是线性的。
其二绘制的间隔理论上要与手机更新频率相适应,一般是60HZ。也就是说,两次绘制之间的时间间隔,应该恰好是16.67ms。用它减去绘制时间,就是线程SnowDrawThread睡眠的时间。通过工具类Choreographer,可以注册系统时钟回调:Choreographer.getInstance().postFrameCallback(...),然后在回调里触发当前帧的绘制。但本程序中,仅以实际效果为依据,看得过去就行,并没有如理论般深入。
(11)Github地址
本示例的完整程序见:https://github.com/VaryJames/01_DynamicBg
Over !
网友评论