????本文將以下雪為例,介紹一種Android上實(shí)現(xiàn)動(dòng)態(tài)背景的方式。動(dòng)態(tài)背景是在單獨(dú)的線程中繪制,因此不會(huì)影響UI主線程。即使主線程包含動(dòng)畫,或者要迅速響應(yīng)用戶的滑動(dòng)、拖拽等,都不會(huì)占用任何繪制時(shí)間。
(1)效果圖
????“一圖抵千言”,先來看看效果動(dòng)圖:

????上圖是下雪背景與ListView的結(jié)合展示,動(dòng)態(tài)的雪花與ListView的滑動(dòng)互不影響。ListView可以替換為任意的View、ViewGroup。
????為防止圖被吞,這里是一個(gè)備份鏈接:https://pan.baidu.com/s/1LiHTyAnwwz4ooaKMtLArGg?pwd=u8h8
(2)主要思想
????這種動(dòng)態(tài)的背景,我不想在UI線程中繪制。一般來說,要讓UI的幀率達(dá)到60fps,那么每一幀的繪制時(shí)間不超過16.67ms。在UI線程中繪制這樣的動(dòng)態(tài)背景,會(huì)嚴(yán)重影響性能。在那些有動(dòng)畫、頻繁交互的場(chǎng)景,更會(huì)雪上加霜。
????于是,想著能不能在線程中繪制?在Android中,要在獨(dú)立的線程中繪制UI,只有兩種辦法。一種是使用SurfaceView,另一種是使用TextureView。SurfaceView擁有獨(dú)立的繪圖表面,和其他View是不能隨意組合到一起的,因而被排除。TextureView則與父布局共用同一個(gè)繪圖表面,因此可以和任意View結(jié)合,滿足了我的需要。
(3) TextureView簡(jiǎn)介
???? 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可以用來展示內(nèi)容流,如來自相機(jī)攝像頭的取景、視頻或OpenGL場(chǎng)景。內(nèi)容流可以來自應(yīng)用進(jìn)程,也可以來自遠(yuǎn)端進(jìn)程。
???? TextureView只有在硬件加速開啟的窗口中才能使用。如果未開啟,那么TextureView什么都不繪制。
????除此之外,再無過多介紹。開始有一些納悶,從這些介紹來看,TextureView似乎是為了相機(jī)、視頻等設(shè)計(jì)的,能滿足我的需要嗎?而且還有硬件加速的限制,這不是有很大的風(fēng)險(xiǎn)嗎?Android手機(jī)的品牌和種類可謂是汗牛充棟,不勝枚舉,如果用戶手機(jī)不支持硬件加速,那不是白瞎嗎?
????帶著這些疑問,做了一些深入的了解和嘗試。首先硬件加速問題,在Android 3.0就支持了硬件加速,Android 4.0默認(rèn)開啟了硬件加速。如下:
android:hardwareAccelerated="true"
????現(xiàn)在已經(jīng)Android 13了,經(jīng)過了這么多年的更新?lián)Q代,市場(chǎng)上絕大部分的手機(jī)應(yīng)該都支持了。從2021-11-23日Google發(fā)布的設(shè)備份額報(bào)告中得知,Android 4.0已經(jīng)是最低系統(tǒng)版本,占比僅為0.4%。所以硬件加速應(yīng)該不是任何阻礙了。
????然后,對(duì)是否支持這種動(dòng)態(tài)繪制做了進(jìn)一步的嘗試,發(fā)現(xiàn)完全沒問題,可以滿足需要。下面先來介紹實(shí)現(xiàn)思路,再介紹具體的類。
(4)基本實(shí)現(xiàn)思路
????首先,如何產(chǎn)生這些雪花,它們的位置如何確定?
????所有的雪花都源于同一張png圖片,不同的雪花大小,是對(duì)原圖進(jìn)行了不同程度的縮放。它們的初始位置和結(jié)束位置,可以根據(jù)需要來設(shè)定,全屏或部分區(qū)域都行。雪花的位置在特定的范圍內(nèi)隨機(jī)設(shè)置。比如初始位置x在[0,1440]內(nèi)隨機(jī),結(jié)束位置在[x-200,x+200]區(qū)域隨機(jī)。
????其次,雪花的運(yùn)動(dòng)軌跡是怎樣的?如何來更新?
????雪花的運(yùn)動(dòng)軌跡是線性的,從隨機(jī)的起始位置,運(yùn)動(dòng)到相應(yīng)的結(jié)束位置。當(dāng)然,這并不是強(qiáng)制的,現(xiàn)實(shí)生活中,雪花的飄落還會(huì)受到風(fēng)力的影響,如果能以某種公式來計(jì)算各個(gè)時(shí)間點(diǎn)的位置,那自然更好。但這更多的是物理、數(shù)學(xué)里的問題,從實(shí)現(xiàn)上來講,和線性的繪制并無區(qū)別。
????雪花的下落有快有慢,這和它們的初始隨機(jī)大小有關(guān)。大的雪花下落快,小的雪花下落慢,這是通過賦予它們不同的初速度來實(shí)現(xiàn)的。雪花的更新,是和整體運(yùn)動(dòng)時(shí)間有關(guān)。每間隔一小段時(shí)間,就更新各雪花的位置,并繪制到畫布上。
????最后,雪花的落地有一種融入的效果,如何來體現(xiàn)?
????在雪花已經(jīng)下落80%的距離后,剩下的20%再加一個(gè)漸出動(dòng)畫。也即是改變它的alpha值,使得落到終點(diǎn)時(shí)alpha=0,剛好看不見。
(5)雪花類SnowFlake
????先來看看構(gòu)造器:
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; //隨機(jī)alpha值,取0.2~1之間
scale = (float) Math.floor(Math.random() * 5 + 6) / 10; //隨機(jī)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;
}
????雪花對(duì)應(yīng)的drawable是static的,被所有的雪花對(duì)象共用。不同的雪花在構(gòu)造時(shí),透明度、大小都在一定范圍內(nèi)隨機(jī)。初始位置及偏移也是如此。
????有些變量暫時(shí)不知道定義并不要緊,后面給出本示例的Github地址,感興趣的朋友可以去下載。
????雪花的初始化,根據(jù)alpha設(shè)置不同的速度和落點(diǎn):
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));
}
}
}
????判斷雪花是否觸底:
/**
* 當(dāng)前雪花是否觸底
*
* @return
*/
private void checkReachBottom() {
if (y >= (int) (endY * 0.8f)) {
isReachBottom = true;
}
}
????判斷雪花是否應(yīng)該死亡,即最終消失:
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;
}
????根據(jù)時(shí)間間隔,更新雪花位置:
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代表著單個(gè)雪花對(duì)象,本小節(jié)的SnowFactory是對(duì)眾多雪花對(duì)象進(jìn)行管理。
????先來看看構(gòu)造器:
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);
}
????創(chuàng)建了一個(gè)定時(shí)器,每隔1s就新增一個(gè)雪花。這個(gè)定時(shí)器是另外一個(gè)線程,負(fù)責(zé)觸發(fā)雪花的生產(chǎn),和繪制所在線程不同。雪花有新增,有更新,有繪制,有消亡,它們的處理并不在同一個(gè)線程中,所以用到了lockObject來處理同步。
????生產(chǎn)雪花:
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
????上面提到,雪花的繪制是在單獨(dú)的線程中,和UI線程不同。本小節(jié)就來介紹一下SnowDrawThread。先看看構(gòu)造器:
public class SnowDrawThread extends Thread {
public SnowDrawThread(SnowFactory factory, TextureView textureView) {
setRunning(true);
this.factory = factory;
this.textureView = textureView;
}
}
????很簡(jiǎn)單,傳入SnowFactory和TextureView對(duì)象。再看看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)提交繪制結(jié)果。繪制完成后,將當(dāng)前線程投入睡眠。睡眠特定時(shí)間后,先清屏,再接著下一幀的繪制,如此重復(fù)。
(8)調(diào)用方
????SnowDrawThread是在TextureView的相關(guān)回調(diào)中調(diào)用,而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的啟動(dòng)和終止就在該mListener的對(duì)應(yīng)回調(diào)里。
????至此,基本內(nèi)容和主要實(shí)現(xiàn)就介紹完了。
(9)擴(kuò)展
????這種實(shí)現(xiàn)思路,可以擴(kuò)展到很多方面。一些基本的平移、旋轉(zhuǎn)、縮放、Alpha漸變等動(dòng)畫,都可以通過它來實(shí)現(xiàn)。特別當(dāng)有循環(huán)動(dòng)畫時(shí),可以減輕主線程的性能壓力。只要有確定的公式,可以根據(jù)它來計(jì)算不同時(shí)間點(diǎn)的位置,都能運(yùn)用本思路。
????近些年比較流行的Lottie動(dòng)畫庫,讓Android的動(dòng)畫有了巨大的飛躍。但它仍然是在主線程中繪制的,這在某些性能要求高的場(chǎng)景很受限制。如果能深入研究一下源碼,將它與本示例中的思路結(jié)合起來,用單獨(dú)的線程來繪制,那可能又會(huì)是另一個(gè)飛躍。
(10)遺憾
????因?yàn)闀r(shí)間和精力的關(guān)系,本示例并沒有做到極致。有一些遺憾:
????其一是雪花的下落理論上要符合重力的規(guī)律,這注定不能是線性的。
????其二繪制的間隔理論上要與手機(jī)更新頻率相適應(yīng),一般是60HZ。也就是說,兩次繪制之間的時(shí)間間隔,應(yīng)該恰好是16.67ms。用它減去繪制時(shí)間,就是線程SnowDrawThread睡眠的時(shí)間。通過工具類Choreographer,可以注冊(cè)系統(tǒng)時(shí)鐘回調(diào):Choreographer.getInstance().postFrameCallback(...),然后在回調(diào)里觸發(fā)當(dāng)前幀的繪制。但本程序中,僅以實(shí)際效果為依據(jù),看得過去就行,并沒有如理論般深入。
(11)Github地址
????本示例的完整程序見:https://github.com/VaryJames/01_DynamicBg
????Over !