美文网首页
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