????本文將以下雪為例早龟,介紹一種Android上實現動態(tài)背景的方式祝沸。動態(tài)背景是在單獨的線程中繪制,因此不會影響UI主線程麻养。即使主線程包含動畫褐啡,或者要迅速響應用戶的滑動、拖拽等鳖昌,都不會占用任何繪制時間备畦。
(1)效果圖
????“一圖抵千言”,先來看看效果動圖:
????上圖是下雪背景與ListView的結合展示许昨,動態(tài)的雪花與ListView的滑動互不影響懂盐。ListView可以替換為任意的View、ViewGroup车要。
????為防止圖被吞,這里是一個備份鏈接:https://pan.baidu.com/s/1LiHTyAnwwz4ooaKMtLArGg?pwd=u8h8
(2)主要思想
????這種動態(tài)的背景崭倘,我不想在UI線程中繪制翼岁。一般來說,要讓UI的幀率達到60fps司光,那么每一幀的繪制時間不超過16.67ms琅坡。在UI線程中繪制這樣的動態(tài)背景,會嚴重影響性能残家。在那些有動畫榆俺、頻繁交互的場景,更會雪上加霜坞淮。
????于是茴晋,想著能不能在線程中繪制?在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了霹疫,經過了這么多年的更新?lián)Q代,市場上絕大部分的手機應該都支持了综芥。從2021-11-23日Google發(fā)布的設備份額報告中得知更米,Android 4.0已經是最低系統(tǒng)版本,占比僅為0.4%毫痕。所以硬件加速應該不是任何阻礙了征峦。
????然后迟几,對是否支持這種動態(tài)繪制做了進一步的嘗試,發(fā)現完全沒問題栏笆,可以滿足需要类腮。下面先來介紹實現思路,再介紹具體的類蛉加。
(4)基本實現思路
????首先蚜枢,如何產生這些雪花,它們的位置如何確定针饥?
????所有的雪花都源于同一張png圖片厂抽,不同的雪花大小,是對原圖進行了不同程度的縮放丁眼。它們的初始位置和結束位置筷凤,可以根據需要來設定,全屏或部分區(qū)域都行苞七。雪花的位置在特定的范圍內隨機設置藐守。比如初始位置x在[0,1440]內隨機,結束位置在[x-200,x+200]區(qū)域隨機蹂风。
????其次卢厂,雪花的運動軌跡是怎樣的?如何來更新惠啄?
????雪花的運動軌跡是線性的慎恒,從隨機的起始位置,運動到相應的結束位置撵渡。當然融柬,這并不是強制的,現實生活中姥闭,雪花的飄落還會受到風力的影響丹鸿,如果能以某種公式來計算各個時間點的位置越走,那自然更好棚品。但這更多的是物理、數學里的問題廊敌,從實現上來講铜跑,和線性的繪制并無區(qū)別。
????雪花的下落有快有慢骡澈,這和它們的初始隨機大小有關锅纺。大的雪花下落快,小的雪花下落慢肋殴,這是通過賦予它們不同的初速度來實現的囤锉。雪花的更新坦弟,是和整體運動時間有關。每間隔一小段時間官地,就更新各雪花的位置酿傍,并繪制到畫布上。
????最后驱入,雪花的落地有一種融入的效果赤炒,如何來體現?
????在雪花已經下落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代表著單個雪花對象图仓,本小節(jié)的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);
}
????創(chuàng)建了一個定時器,每隔1s就新增一個雪花救崔。這個定時器是另外一個線程惶看,負責觸發(fā)雪花的生產,和繪制所在線程不同六孵。雪花有新增纬黎,有更新,有繪制劫窒,有消亡本今,它們的處理并不在同一個線程中,所以用到了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線程不同。本小節(jié)就來介紹一下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漸變等動畫慌随,都可以通過它來實現。特別當有循環(huán)動畫時躺同,可以減輕主線程的性能壓力阁猜。只要有確定的公式,可以根據它來計算不同時間點的位置笋籽,都能運用本思路蹦漠。
????近些年比較流行的Lottie動畫庫椭员,讓Android的動畫有了巨大的飛躍车海。但它仍然是在主線程中繪制的,這在某些性能要求高的場景很受限制。如果能深入研究一下源碼侍芝,將它與本示例中的思路結合起來研铆,用單獨的線程來繪制,那可能又會是另一個飛躍州叠。
(10)遺憾
????因為時間和精力的關系棵红,本示例并沒有做到極致。有一些遺憾:
????其一是雪花的下落理論上要符合重力的規(guī)律咧栗,這注定不能是線性的逆甜。
????其二繪制的間隔理論上要與手機更新頻率相適應,一般是60HZ致板。也就是說交煞,兩次繪制之間的時間間隔,應該恰好是16.67ms斟或。用它減去繪制時間素征,就是線程SnowDrawThread睡眠的時間。通過工具類Choreographer萝挤,可以注冊系統(tǒng)時鐘回調:Choreographer.getInstance().postFrameCallback(...)御毅,然后在回調里觸發(fā)當前幀的繪制。但本程序中怜珍,僅以實際效果為依據端蛆,看得過去就行,并沒有如理論般深入酥泛。
(11)Github地址
????本示例的完整程序見:https://github.com/VaryJames/01_DynamicBg
????Over !