美文网首页
19_Android动态背景

19_Android动态背景

作者: 刘加城 | 来源:发表于2023-02-11 07:06 被阅读0次

    本文将以下雪为例,介绍一种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 !

相关文章

网友评论

      本文标题:19_Android动态背景

      本文链接:https://www.haomeiwen.com/subject/doslkdtx.html