重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
1、界面搭建过程中各种大小单位
公司主营业务:做网站、成都网站建设、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。创新互联建站是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。创新互联建站推出祥符免费做网站回馈大家。
Android:通常采用dp设置View宽高(和px像素的换算关系是dp值 × density逻辑密度),sp设置字体大小(会随着系统字体设置的大小而改变)。
Flutter:没有具体的大小单位描述, 和尺寸相关的MediaQueryData类中较为重要的几个值如下:
(一)devicePixelRatio(设备像素比),对应Android中的density
(二)size.width和height,设备的逻辑像素宽高,并非绝对物理像素(例如iphone6的设备像素比是2,通过size获取到的逻辑像素宽高为375 × 667,实际物理像素则为750 × 1334,即分辨率)
(三)textScaleFactor:单位逻辑像素字体像素数,默认为1,设置成1.5则字体变大50%,如果想让Text组件的字体大小不随系统设置的变化而变化,需将这个值设定成固定值1
UI适配解决方案:
1、采用ScreenUtil插件,初始化时候传入设计稿大小,当发现一屏显示的大小有差异时候采用插件提供的setWidth和setHeight来设置具体的宽高(会根据设计稿大小和实际设备逻辑像素宽高比进行缩放)。
2、TextButton、Text等按钮和文本组件,通过设置字体大小和内边距来控制整体的宽高,而非固定其宽高。
2、本地资源文件的引用方式
Android:图片通常存放在res/mipmap或res/drawable下,不同分辨率对应不同后缀名,如mipmap-hdpi、mipmap-xhdpi
Flutter:需在pubspec.yaml中配置,如下图所示
如果只配置父级目录例如(assets/images/common_status)则无法再存放不同尺寸的图片。不同尺寸的图片需建立对应的2.0x、3.0x目录后存放,设备在读取时候会自行根据分辨率去找对应的图片,弊端是每有一张图片就需在pubspec.yaml文件中声明这些图片
1、常用布局的对比
使用下来其他组件大致还算方便,但是相对布局而言使用便利程度上Android原生完胜,ConstraintLayout内部的所有子View可以设置互相之间的位置依赖关系。
而Flutter的Stack组件内部的Children只能通过外层包裹 Align后 固定位置,比如 Alignment.topLeft、Alignment.bottomRight 等。遇到复杂的堆叠布局需要通过外层包裹 Positioned 组件后设置固定的 top 和 left 距离以达到效果,内部子组件之间无法设置位置关联关系。
2、一些常用属性设置上的差异:
Margin外边距
Android:直接在布局文件对View设置android:layout_marginStart、android:layout_marginTop
Flutter:需嵌套 Container 组件并在内部设置具体的 margin 值
Padding内边距
Android:TextView、ImageView、各种Layout都可以直接在属性上设置android:paddingStart
Flutter:需嵌套 Padding 组件并在内部设置具体的值
组件的可见性
Android:每个view都可以通过setVisibility来设置可见、隐藏或者隐藏但占位
Flutter:没有单独设置组件是否显示的api,只能通过 bool 值控制是否添加该组件
事件监听
Android:常规的setOnClickListener和setOnLongClickListener设置单击和长按事件
Flutter:在需要添加事件监听的组件外层嵌套 InkWell 或 GestureDetector 并设置 onTap 等
3、生命周期
Android:
Activity和Fragment各自有完整的生命周期链路onCreate、onStart、onResume、onPause、onDestroy等
Flutter:
万物皆组件,组件继承 WidgetsBindingObserver 并重写 didChangeAppLifecycleState 函数进行监听
退回桌面依次执行inactive 》= paused,此时界面不可见用户不可操作,从桌面重新进入app执行resumed,状态较少如需在某些条件下触发特定操作可能要找别的方案,比如发通知之类的
对于Android开发者来说,我们或多或少有了解过Android图像显示的知识点,刚刚学习Android开发的人会知道,在Actvity的onCreate方法中设置我们的View后,再经过onMeasure,onLayout,onDraw的流程,界面就显示出来了;对Android比较熟悉的开发者会知道,onDraw流程分为软件绘制和硬件绘制两种模式,软绘是通过调用Skia来操作,硬绘是通过调用Opengl ES来操作;对Android非常熟悉的开发者会知道绘制出来的图形数据最终都通过GraphiBuffer内共享内存传递给SurfaceFlinger去做图层混合,图层混合完成后将图形数据送到帧缓冲区,于是,图形就在我们的屏幕显示出来了。
但我们所知道的Activity或者是应用App界面的显示,只属于Android图形显示的一部分。同样可以在Android系统上展示图像的WebView,Flutter,或者是通过Unity开发的3D游戏,他们的界面又是如何被绘制和显现出来的呢?他们和我们所熟悉的Acitvity的界面显示又有什么异同点呢?我们可以不借助Activity的setView或者InflateView机制来实现在屏幕上显示出我们想要的界面吗?Android系统显示界面的方式又和IOS,或者Windows等系统有什么区别呢?……
去探究这些问题,比仅仅知道Acitvity的界面是如何显示出来更加的有价值,因为想要回答这些问题,就需要我们真正的掌握Android图像显示的底层原理,当我们掌握了底层的显示原理后,我们会发现WebView,Flutter或者未来会出现的各种新的图形显示技术,原来都是大同小异。
我会花三篇文章的篇幅,去深入的讲解Android图形显示的原理,OpenGL ES和Skia的绘制图像的方式,他们如何使用,以及他们在Android中的使用场景,如开机动画,Activity界面的软件绘制和硬件绘制,以及Flutter的界面绘制。那么,我们开始对Android图像显示原理的探索吧。
在讲解Android图像的显示之前,我会先讲一下屏幕图像的显示原理,毕竟我们图像,最终都是在手机屏幕上显示出来的,了解这一块的知识会让我们更容易的理解Android在图像显示上的机制。
图像显示的完整过程,分为下面几个阶段:
图像数据→CPU→显卡驱动→显卡(GPU)→显存(帧缓冲)→显示器
我详细介绍一下这几个阶段:
实际上显卡驱动,显卡和显存,包括数模转换模块都是属于显卡的模块。但为了能能详细的讲解经历的步骤,这里做了拆分。
当显存中有数据后,显示器又是怎么根据显存里面的数据来进行界面的显示的呢?这里以LCD液晶屏为例,显卡会将显存里的数据,按照从左至右,从上到下的顺序同步到屏幕上的每一个像素晶体管,一个像素晶体管就代表了一个像素。
如果我们的屏幕分辨率是1080x1920像素,就表示有1080x1920个像素像素晶体管,每个橡素点的颜色越丰富,描述这个像素的数据就越大,比如单色,每个像素只需要1bit,16色时,只需要4bit,256色时,就需要一个字节。那么1080x1920的分辨率的屏幕下,如果要以256色显示,显卡至少需要1080x1920个字节,也就是2M的大小。
刚刚说了,屏幕上的像素数据是从左到右,从上到下进行同步的,当这个过程完成了,就表示一帧绘制完成了,于是会开始下一帧的绘制,大部分的显示屏都是以60HZ的频率在屏幕上绘制完一帧,也就是16ms,并且每次绘制新的一帧时,都会发出一个垂直同步信号(VSync)。我们已经知道,图像数据都是放在帧缓冲中的,如果帧缓冲的缓冲区只有一个,那么屏幕在绘制这一帧的时候,图像数据便没法放入帧缓冲中了,只能等待这一帧绘制完成,在这种情况下,会有很大了效率问题。所以为了解决这一问题,帧缓冲引入两个缓冲区,即 双缓冲机制 。双缓冲虽然能解决效率问题,但会引入一个新的问题。当屏幕这一帧还没绘制完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,显卡的像素同步模块就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象。
为了解决撕裂问题,就需要在收到垂直同步的时候才将帧缓冲中的两个缓冲区进行交换。Android4.1黄油计划中有一个优化点,就是CPU和GPU都只有收到垂直同步的信号时,才会开始进行图像的绘制操作,以及缓冲区的交换工作。
我们已经了解了屏幕图像显示的原理了,那么接着开始对Android图像显示的学习。
从上一章已经知道,计算机渲染界面必须要有GPU和帧缓冲。对于Linux系统来说,用户进程是没法直接操作帧缓冲的,但我们想要显示图像就必须要操作帧缓冲,所以Linux系统设计了一个虚拟设备文件,来作为对帧缓冲的映射,通过对该文件的I/O读写,我们就可以实现读写屏操作。帧缓冲对应的设备文件于/dev/fb* ,*表示对多个显示设备的支持, 设备号从0到31,如/dev/fb0就表示第一块显示屏,/dev/fb1就表示第二块显示屏。对于Android系统来说,默认使用/dev/fb0这一个设帧缓冲作为主屏幕,也就是我们的手机屏幕。我们Android手机屏幕上显示的图像数据,都是存储在/dev/fb0里,早期AndroidStuio中的DDMS工具实现截屏的原理就是直接读取/dev/fb0设备文件。
我们知道了手机屏幕上的图形数据都存储在帧缓冲中,所以Android手机图像界面的原理就是将我们的图像数据写入到帧缓冲内。那么,写入到帧缓冲的图像数据是怎么生成的,又是怎样加工的呢?图形数据是怎样送到帧缓冲去的,中间经历了哪些步骤和过程呢?了解了这几个问题,我们就了解了Android图形渲染的原理,那么带着这几个疑问,接着往下看。
想要知道图像数据是怎么产生的,我们需要知道 图像生产者 有哪些,他们分别是如何生成图像的,想要知道图像数据是怎么被消费的,我们需要知道 图像消费者 有哪些,他们又分别是如何消费图像的,想要知道中间经历的步骤和过程,我们需要知道 图像缓冲区 有哪些,他们是如何被创建,如何分配存储空间,又是如何将数据从生产者传递到消费者的,图像显示是一个很经典的消费者生产者的模型,只有对这个模型各个模块的击破,了解他们之间的流动关系,我们才能找到一条更容易的路径去掌握Android图形显示原理。我们看看谷歌提供的官方的架构图是怎样描述这一模型的模块及关系的。
如图, 图像的生产者 主要有MediaPlayer,CameraPrevier,NDK,OpenGl ES。MediaPlayer和Camera Previer是通过直接读取图像源来生成图像数据,NDK(Skia),OpenGL ES是通过自身的绘制能力生产的图像数据; 图像的消费者 有SurfaceFlinger,OpenGL ES Apps,以及HAL中的Hardware Composer。OpenGl ES既可以是图像的生产者,也可以是图像的消费者,所以它也放在了图像消费模块中; 图像缓冲区 主要有Surface以及前面提到帧缓冲。
Android图像显示的原理,会仅仅围绕 图像的生产者 , 图像的消费者 , 图像缓冲区 来展开,在这一篇文章中,我们先看看Android系统中的图像消费者。
SurfaceFlinger是Android系统中最重要的一个图像消费者,Activity绘制的界面图像,都会传递到SurfaceFlinger来,SurfaceFlinger的作用主要是接收图像缓冲区数据,然后交给HWComposer或者OpenGL做合成,合成完成后,SurfaceFlinger会把最终的数据提交给帧缓冲。
那么SurfaceFlinger是如何接收图像缓冲区的数据的呢?我们需要先了解一下Layer(层)的概念,一个Layer包含了一个Surface,一个Surface对应了一块图形缓冲区,而一个界面是由多个Surface组成的,所以他们会一一对应到SurfaceFlinger的Layer中。SurfaceFlinger通过读取Layer中的缓冲数据,就相当于读取界面上Surface的图像数据。Layer本质上是 Surface和SurfaceControl的组合 ,Surface是图形生产者和图像消费之间传递数据的缓冲区,SurfaceControl是Surface的控制类。
前面在屏幕图像显示原理中讲到,为了防止图像的撕裂,Android系统会在收到VSync垂直同步时才会开始处理图像的绘制和合成工作,而Surfaceflinger作为一个图像的消费者,同样也是遵守这一规则,所以我们通过源码来看看SurfaceFlinger是如何在这一规则下,消费图像数据的。
SurfaceFlinger专门创建了一个EventThread线程用来接收VSync。EventThread通过Socket将VSync信号同步到EventQueue中,而EventQueue又通过回调的方式,将VSync信号同步到SurfaceFlinger内。我们看一下源码实现。
上面主要是SurfaceFlinger初始化接收VSYNC垂直同步信号的操作,主要有这几个过程:
经过上面几个步骤,我们接收VSync的初始化工作都准备好了,EventThread也开始运转了,接着看一下EventThread的运转函数threadLoop做的事情。
threadLoop主要是两件事情
mConditon又是怎么接收VSync的呢?我们来看一下
可以看到,mCondition的VSync信号实际是DispSyncSource通过onVSyncEvent回调传入的,但是DispSyncSource的VSync又是怎么接收的呢?在上面讲到的SurfaceFlinger的init函数,在创建EventThread的实现中,我们可以发现答案—— mPrimaryDispSync 。
DispSyncSource的构造方法传入了mPrimaryDispSync,mPrimaryDispSync实际是一个DispSyncThread线程,我们看看这个线程的threadLoop方法
DispSyncThread的threadLoop会通过mPeriod来判断是否进行阻塞或者进行VSync回调,那么mPeriod又是哪儿被设置的呢?这里又回到SurfaceFlinger了,我们可以发现在SurfaceFlinger的 resyncToHardwareVsync 函数中有对mPeriod的赋值。
可以看到,这里最终通过HWComposer,也就是硬件层拿到了period。终于追踪到了VSync的最终来源了, 它从HWCompser产生,回调至DispSync线程,然后DispSync线程回调到DispSyncSource,DispSyncSource又回调到EventThread,EventThread再通过Socket分发到MessageQueue中 。
我们已经知道了VSync信号来自于HWCompser,但SurfaceFlinger并不会一直监听VSync信号,监听VSync的线程大部分时间都是休眠状态,只有需要做合成工作时,才会监听VSync,这样即保证图像合成的操作能和VSync保持一致,也节省了性能。SurfaceFlinger提供了一些主动注册监听VSync的操作函数。
可以看到,只有当SurfaceFlinger调用 signalTransaction 或者 signalLayerUpdate 函数时,才会注册监听VSync信号。那么signalTransaction或者signalLayerUpdate什么时候被调用呢?它可以由图像的生产者通知调用,也可以由SurfaceFlinger根据自己的逻辑来判断是否调用。
现在假设App层已经生成了我们界面的图像数据,并调用了 signalTransaction 通知SurfaceFlinger注册监听VSync,于是VSync信号便会传递到了MessageQueue中了,我们接着看看MessageQueue又是怎么处理VSync的吧。
MessageQueue收到VSync信号后,最终回调到了SurfaceFlinger的 onMessageReceived 中,当SurfaceFlinger接收到VSync后,便开始以一个图像消费者的角色来处理图像数据了。我们接着看SurfaceFlinger是以什么样的方式消费图像数据的。
VSync信号最终被SurfaceFlinger的onMessageReceived函数中的INVALIDATE模块处理。
INVALIDATE的流程如下:
handleMessageTransaction的处理比较长,处理的事情也比较多,它主要做的事情有这些
handleMessageRefresh函数,便是SurfaceFlinger真正处理图层合成的地方,它主要下面五个步骤。
我会详细介绍每一个步骤的具体操作
合成前预处理会判断Layer是否发生变化,当Layer中有新的待处理的Buffer帧(mQueuedFrames0),或者mSidebandStreamChanged发生了变化, 都表示Layer发生了变化,如果变化了,就调用signalLayerUpdate,注册下一次的VSync信号。如果Layer没有发生变化,便只会做这一次的合成工作,不会注册下一次VSync了。
重建Layer栈会遍历Layer,计算和存储每个Layer的脏区, 然后和当前的显示设备进行比较,看Layer的脏区域是否在显示设备的显示区域内,如果在显示区域内的话说明该layer是需要绘制的,则更新到显示设备的VisibleLayersSortedByZ列表中,等待被合成
rebuildLayerStacks中最重要的一步是 computeVisibleRegions ,也就是对Layer的变化区域和非透明区域的计算,为什么要对变化区域做计算呢?我们先看看SurfaceFlinger对界面显示区域的分类:
还是以这张图做例子,可以看到我们的状态栏是半透明的,所以它是一个opaqueRegion区域,微信界面和虚拟按键是完全不透明的,他是一个visibleRegion,除了这三个Layer外,还有一个我们看不到的Layer——壁纸,它被上方visibleRegion遮挡了,所以是coveredRegion
对这几个区域的概念清楚了,我们就可以去了解computeVisibleRegions中做的事情了,它主要是这几步操作:
先来看下效果吧:
我们来分析这个view需要实现哪些效果。
别害怕有这么多的功能,我们一个一个来实现。首先是刻度尺,这个简单。由于完整的刻度尺是比屏幕宽度大的,因此我们先来了解几个概念:
这里手机屏幕的宽度是width,刻度尺的宽度的时maxWidth,我们其实只需要绘制手机屏幕可见的部分就可以了,这里的offset表示手机屏幕的左边与刻度尺左边的偏移量。
了解了这个概念,我们就来开始写吧,定义一个View,处理下构造都指向3个参数的那个,然后统一做初始化:
我们在onMeasure中处理了wrap_content的高度。然后在onSizeChanged中获取尺寸参数:
接着就开始绘制吧:
这里的titles代表了刻度的标识,每一个元素代表一个刻度(这里我字节写死了,实际上可以通过方法set,也不一定是时间,能代表刻度的都可以)。通过rate设置长短刻度的比例,这里我设置了1:1。运行一下看看,目前仅仅能看到从0开始,看不到完整的刻度尺,我们需要实现touch事件产生移动才有效果。
我们重写onTouchEvent来实现滑动效果:
我们计算出每次move事件的X方向的变化量dx,然后通过这个dx改变offset,并且处理一下边界的情况。然后调用postInvalidate刷新界面。
运行一下看看!现在我们可以滑动刻度尺了。但是好像还有点问题,平时我们使用ScrollView的时候用力划一下,可以看到手指离开了屏幕,但是内容还可以继续滚动。而目前我们自定义的这个view只能通过手指滑动,如果手指离开屏幕就不能滑动了。这样的体验显然不够好,我们来实现这个惯性滑动的效果吧!
要实现惯性滑动,我们需要用到两个类:VelocityTracker,OverScroller。
VelocityTracker简介
view滑动助手类OverScroller
velocityTracker.computeCurrentVelocity方法的第二个参数表示最大惯性速度,这里我设置8000,避免刻度尺过快的滑动。通过调用scroller.fling方法将计算出的速度交给scroller,然后在computeScroll方法中获取当前值,并与上一次的值做差算出变化量dx,同样用这个dx变化offset刷新界面实现滑动效果。
刻度尺完成了,接下来是不可选的灰色区域。我采用两个int值表示在刻度尺的区域,刻度尺的每个刻度表示一个最小单位,前一个int表示在刻度尺的起始位置,后一个int表示占据的刻度数量。
我用一个list存放设置的不可选区域,然后在另一个list中存放转换成RectF的位置信息。这里的RectF是在相对于整体刻度尺而言的,因此绘制到屏幕的时候需要减去offset,并且需要考虑只有部分在屏幕可见的情况。避免在onDraw方法中创建过多临时变量,我声明一个成员变量tempRect,用来保存绘制时的临时参数。
完成了不可选区域,可选区域也是同样的。由于只能有一个可选区域,我们只需要定义一个RectF。额外需要考虑与不可选区域相交时会变色,我定了一个overlapping表示是否相交,通过RectF的intersects方法判断。
通过前面的分析,我们知道这个view中的事件有很多种:点击,移动刻度尺,移动选中区域,扩展选中区域。我们定义这四种类型便于后续的事件处理:
然后改造一下onTouchEvent:
performClick会在你重写onTouchEvent时as提示你需要重写的方法,因为你可能没有考虑到如果给这个view设置OnClickListener的情况。如果你没有在onTouchEvent中调用performClick,那么setOnClickListener方法就失效了。
你可能注意到这一次比较复杂,并且还有一个linking字段,表示是否正在联动,我解释一下这个联动的概念:通过gif其实你可能注意到,当我移动或者扩展选中区域的时候,如果移动到了屏幕的边界,后面的刻度尺就会跟着移动,实际上这个时候选中区域在屏幕中的位置没有改变,只是刻度尺移动了。一开始我也是通过dx来改变offset,但是存在一个问题,移动到屏幕边缘之后,手指可以移动的区域已经很小了,不会产生足够的dx(手指不移动的话,不会有新的touch事件产生)。最好的体验是我把手机移动到屏幕边缘,刻度尺就会自己按照一定的速率移动直到最大offset或者最小offset。于是我使用了Handler,当满足条件后发送消息,表示开始进行联动,会按照固定速度产生一个dx改变offset。当然,在离开屏幕边缘的时候还需要及时取消handler的任务。
至此,功能基本已经实现了,运行一下看看效果吧~
后面需要做什么那?现在这个view只能自己玩,我需要它与其他view有交互,比如选中什么区域,状态的改变生么的。
声明两个接口,并在适当时候回调它们的方法,这样外部就能感知view的状态变化。
后面的话就是根据业务添加一些api了,例如添加不可选区域,改变刻度范围什么,一切都看需求了。
想学习更多Android知识,或者获取相关资料请加入Android开发交流群:1018342383。 有面试资源系统整理分享,Java语言进阶和Kotlin语言与Android相关技术内核,APP开发框架知识, 360°Android App全方位性能优化。Android前沿技术,高级UI、Gradle、RxJava、小程序、Hybrid、 移动架构师专题项目实战环节、React Native、等技术教程!架构师课程、NDK模块开发、 Flutter等全方面的 Android高级实践技术讲解。还有在线答疑