19_Android動(dòng)態(tài)背景

????本文將以下雪為例,介紹一種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)圖:

動(dòng)態(tài)下雪背景

????上圖是下雪背景與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 !

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容