19_Android動態(tài)背景

????本文將以下雪為例早龟,介紹一種Android上實現動態(tài)背景的方式祝沸。動態(tài)背景是在單獨的線程中繪制,因此不會影響UI主線程麻养。即使主線程包含動畫褐啡,或者要迅速響應用戶的滑動、拖拽等鳖昌,都不會占用任何繪制時間备畦。

(1)效果圖

????“一圖抵千言”,先來看看效果動圖:

動態(tài)下雪背景

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載欺税,如需轉載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末揭璃,一起剝皮案震驚了整個濱河市晚凿,隨后出現的幾起案子,更是在濱河造成了極大的恐慌瘦馍,老刑警劉巖歼秽,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異情组,居然都是意外死亡燥筷,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門院崇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肆氓,“玉大人,你說我怎么就攤上這事底瓣⌒痪荆” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拨扶。 經常有香客問我凳鬓,道長研儒,這世上最難降的妖魔是什么牛郑? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮诊赊,結果婚禮上匹颤,老公的妹妹穿的比我還像新娘仅孩。我一直安慰自己,他們只是感情好印蓖,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布杠氢。 她就那樣靜靜地躺著,像睡著了一般另伍。 火紅的嫁衣襯著肌膚如雪鼻百。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天摆尝,我揣著相機與錄音温艇,去河邊找鬼。 笑死堕汞,一個胖子當著我的面吹牛勺爱,可吹牛的內容都是我干的。 我是一名探鬼主播讯检,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼琐鲁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了人灼?” 一聲冷哼從身側響起围段,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎投放,沒想到半個月后奈泪,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡灸芳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年涝桅,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烙样。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡冯遂,死狀恐怖,靈堂內的尸體忽然破棺而出谒获,到底是詐尸還是另有隱情蛤肌,我是刑警寧澤壁却,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站寻定,受9級特大地震影響,放射性物質發(fā)生泄漏精耐。R本人自食惡果不足惜狼速,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卦停。 院中可真熱鬧向胡,春花似錦、人聲如沸惊完。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽小槐。三九已至拇派,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凿跳,已是汗流浹背件豌。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留控嗜,地道東北人茧彤。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像疆栏,于是被迫代替她去往敵國和親曾掂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容