title: '深入理解android2-WMS,控件-图床版'
date: 2020-03-08 16:22:42
tags:
typora-root-url: /深入理解android2-WMS-控件
typora-copy-images-to: /深入理解android2-WMS-控件
WMS主要负责两个功能, 一是负责窗口的管理,如窗口的增加删除,层级二是负责全局事件的派发如触摸点击事件
先简单介绍几个重要的类
IWindowSession 进程唯一的是一个匿名binder通过他向WMS请求窗口 *** 作
surface 绘画时,canvas会把内容绘制到surface里surface是有surfaceFlinger提供给客户端的
WindowManagerLayoutParams 集成自ViewGroupLayoutParams用来指明client端的窗口的一些属性最重要的是type 根据这属性来对多个窗口进程ZOrder的排序
windowToken向WMS添加的窗口令牌每个窗口都要有一个令牌
IWindow 是client提供给WMS的继承自binderWMS通过IWindow对象来主动发起client端的事件
窗口的本周就是进行绘制所使用的surface,客户端向WMS添加窗口的过程,就是WMS为客户端分配surface的过程
ui框架层就是使用surface上绘制ui元素及响应输入事件
WMS负责surface的分配窗口的层级顺序
surfaceFlinger负责将多个Surface混合并输出
WMS有SystemServer 进程启动他和AMS其实是运行于一个进程中的只是分别有各自的线程
上边传入了两个handler这里就使用windowManager的handler来创建WMS也就是在一个handerThread线程中创建
用来管理每个窗口的事件输入也就是把输入事件转发到正确的窗口
能获取显示系统的同步信号用来驱动动画的渲染
所有窗口动画的总管,在mChoreographer的驱动下渲染所有动画
只有PhoneWindowManager一个实现定义了很多窗口相关的策略是最重要的成员,比如负责窗口的zorder顺序
zorder就是各个窗口在z轴的值越大越在屏幕上层窗口就是根据zorder值一层一层堆在一起
可以绘制的屏幕列表默认是只有1个
管理所以窗口的显示令牌token,每个窗口都要属于一个token这里的IBinder 是
表示所有Activity的token AppWindowToken是WindowToken的子类,这个list的顺序和AMS中对mHistory列表中activity的顺序是一样的 反应了系统中activity的叠加顺序也就是说所有窗口都有WindowToken而Activity对应的窗口则多了AppWindowToken
每个窗口都对应一个WindowState存储改窗口的状态信息(这就和AMS中对每个activity抽象成ActivityRecord一样)
这里的iBinder 是IWIndow类
Session 是WMS提供给客户端来与WMS进行交互的,这是匿名binder为了减轻WMS的负担客户端通过IWindowManageropenSession 拿到他的代理然后通过代理与WMS交互每个进程唯一
客户端通过IWindowSessionadd 来添加窗口 iWindowSession 是同aidl形成的最终到了WMSaddWindow
这里总的来说就是确立了客户窗口的WindowTokenWindowState和DisplayContent 并都保存了起来同时根据layoutparamstype进行了些窗口等级的判断
WindowToken将同一个应用组件的窗口安排在一起一个应用组件可以是Activity,InputMethod
WindowToken使应用组件在变更窗口时必须与自己的WindowToken匹配
这里主要是为了处理窗口的层级关系而设立的
只要是一个binder对象都可以作为token向wms声明wms会把这个binder对应起一个WindowToken其实就是把客户端的binder和wms里的一个WindowToken对象进行了绑定
因为Activity比较复杂,因此WMS为Activity实现了WindowToken的子类 appwindowtoken同时在AMS启动Activity的ActivityStackstartActivityLocked里声明token
然后在activityStackrealStartActivityLocked里在发给用户进程,然后用户在通过这个binder和WMS交互时带过来
activity则在 activityStack 线程的handleResumeActivity 里把Activity 对应的窗口,加入到wMS中
取消token 也是在AMS中 ,也就是说, AMS负责avtivity的token向WMS的添加和删除
当然Activity的 rappToken 是 IApplicationTokenStub ,他里边有一系列的窗口相关的通知回调
这里总结下 AMS在创建Activity的ActivityRecord时,创建了他的appToken,有把appToken传送给WMSWMS对应匹配为APPWindowToken,最后还把这个appToken发送给activity因此AMS就通过ActivityRecord就可有直接 *** 作WMS对该窗口的绘制如图
每个window在WMS里都抽象成了WindowState他包含一个窗口的所有属性WindowState在客户端对应的则是iWidowstub类iWidowstub有很多窗口通知的回调
WindowState被保存在mWindowMap里这是整个系统所有窗口的一个全集
HashMap<IBinder, WindowToken> mTokenMap 这里是 IApplicationToken(客户端)和WindowToken的映射
HashMap<IBinder, WindowState> mWindowMap 这里是IWidow(客户端)和WindowState的映射,并且WMS通过这个IWindow 来回调客户端的方法
上图可以看出每个activity 只有一个ActivityRecord也只有一个AppToken,也就只有一个WindowToken而一个acitvity可能有多个窗口每个窗口对应一个WindowState
WindowToken用来和AMS交换 而WindowState对应的iWindow则是WMS来与客户端交互的
窗口显示次序就是窗口在Z轴的排了因为窗口是叠加在一起的因此就需要知道哪些显示在上边,哪些在下边这个由WindowState构造时确定
可见分配规则是由WindowManagerPolicy mPolicy来决定的产生 mBaseLayer和mSubLayer mBaseLayer决定该窗口和他的子窗口在所有窗口的显示位置 mSubLayer决定子窗口在同级的兄弟窗口的显示位置值越高显示约靠上
WindowState 产生了他自己这个窗口的layer值后在添加窗口的时候就会把所有窗口按layer排序插入mWindows列表中,在通过 adjustWallpaperWindowsLocked();进行层级调整
当客户端通过IWindowsessionadd后,客户端还没有获得Surface只有在执行IWindowsessionrelayout后客户端才获得了一块Surface IWindowsessionrelayout根据客户端提供的参数,为客户端提供surface具体实现是WMSrelayoutWindow
总的来说就是根据用户传入的参数,更新WindowState然后遍历所有窗口布局在设置合适的Surface尺寸,在返回给用户端
performLayoutAndPlaceSurfacesLocked 会循环调用6次里边的逻辑大概如下
这里主要下,因为之前加了锁requestTraversalLocked他又会重复执行performLayoutAndPlaceSurfacesLocked();因此会重复循环执行布局
布局这部分就记个原理吧
布局完成后客户端的尺寸和surface都得到了就可以绘制 了WMS会通知客户端布局发送变化
总结,WMS 负责管理所有的窗口包括系统窗口和APP窗口,而窗口必须有一个WindowToken所为标识符同时WMS为每个窗口创建一个WindowState类,这是窗口在服务端的抽象WindowState则绑定了一个客户端的IWindow类,WMS通过这个IWindow 向APP发送消息
AMS在启动Activity的时候把ActivityRecordtoken 通过wmsaddtoken 注册到WMS又把这个token发送到APP端因此三方可以通过这个token正确找到对应的数据
WMS负责给所以窗口按ZOrder排序,确定窗口的尺寸,提供绘画用的surface
Activity的窗口是先wmsaddtoken 建立windowToken关系 wmsaddWindow 添加串口, WMSrelayout获取surface 完成
一个windowToken对应一个Activity 但是可能对应多个windowSatate也就是对应多个窗口
是view树的根实现类是viewRootImpl但是他不是view他是用来和WMS进行交流的管理者viewRootImpl内部有个IWindowSession,是WMS提供的匿名binder,同时还有个iWindow子类,用来让WMS给viewr发消息 view通过ViewRoot向WMS发消息WMS在通过IWIndow 向APP发消息 每个View树只有一个ViewRoot,每个Activity也只有一个ViewRoot UI绘制,事件传递都是通过ViewRoot
实现类是PhoneWindow Activity和View的沟通就是通过WindowActivity实现window的各种回调一个Activity也对应一个PhoneWindow也对应一个View树
Docerview 就是View树的根这是一个View 他由PhoneWindow管理 下文的WindowManager也由phoneWindow管理
他还管理window的属性 WindowManagerlayoutparams
他是一个代理类他集成自ViewManager他的实现是WindowManagerImpl这是每个Activity都有一个但是他只是把工作委托给了 WindowManagerGlobal来实现 他负责添加删除窗口,更新窗口并控制窗口的补件属性WindowManagerLayoutparams
是进程唯一的负责这个进程的窗口管理他里边有三个集合保存这个进程所有窗口的数据这里的每个数据根据index得到的是同一个Activity属性所有的WindowManager的 *** 作都转到他这里来
private final ArrayList<View> mViews 每个view是个跟节点
private final ArrayList<ViewRootImpl> mRoots view对应的viewRoot
private final ArrayList<WindowManagerLayoutParams> mParams 窗口的layoutparams属性每个窗口一个
对于一个acitivity对象永远对应一个PhoneWindow,一个WindowManagerImpl,一个WMS端的APPWindowToken,一个AMS里的ActivityRecord(但是如果一个activity在栈里有多个对象,就有多个ActivityRecord和AppWindowToken),acitvity 的默认窗口的view树是DocerView
一个窗口 对应一个ViewRoot,一个View树一个WindowManagerLayoutParams,一IWindow(WMS回调app)一个WSM端的WindowSatate
但是一个Activity可以有多个窗口,因此对应WMS里可能有多个WindowSatate这些WindowState都对应一个AppWindowToken
一个Activity可能被加载多次因此在AMS中可能有多个ActivityRecord对应这个activit的多个对象
但是一个进程则对应一个WindowManagerGlobal一个ActivityThread(主线程)一个ApplicationThread(AMS调用app)一个iWindowSession(viewroot向WMS发消息)
这里的区别就是 app与AMS 的交互是以进程之间进行通信而App与WMS的交互则是以窗口作为通信基础
当Activity由AMS启动时ActivityThread 通过handleResumeActivity执行resume相关的 *** 作这个函数首先是执行activityresume, 此时activity 对应的view树已经建立完成(oncreate中建立,PhoneWindow也创建了)需要把activity的窗口添加到WMS中去管理
这里的wm是WindowManager是每个activity一个他内部会调用WindowManagerGlobaladdView
WindowManagerGlobaladdView
这里会为窗口创建ViewRootImpl 并把viewViewRootImplWindowMaLayoutParams都保存在WindowManagerGlobal中, 并通过ViewRootImpl向WMS添加窗口
如果这个窗口是子窗口(wparamstype >= WindowManagerLayoutParamsFIRST_SUB_WINDOW &&
wparamstype <= WindowManagerLayoutParamsLAST_SUB_WINDOW)
就把子窗口的token设为父窗口的token否则就是所属activity的token
在来个图
在这里我们看到我们通过mWindowManager = (WindowManager) mContextgetSystemService(ContextWINDOW_SERVICE); 拿到的并不是远程的WMS而是本地的WindowManagerImpl 他又把请求转发给WindowManagerGlobal ,而WindowManagerGlobal作为进程单实例又是吧请求转给对应窗口的ViewRootImplViewRootImpl通过WMS的IWindowSession 把数据发给WMS
ViewRootImpl用来沟通View和WMS并接受WMS的消息这是双向的binder通信作为整个空间树的根部,控件的测量,布局,绘制,输入时间的派发都由ViewRootImpl来触发
ViewRootImpl由WindowManagerGlobal创建,是在activityThreadhandleResumeActivity时,先执行activityresume在调用wmaddView 就会执行WindowManagerGlobaladdView里创建ViewRootImpl,此时是在ui线程中
ViewRootImpl里的mView属性host属性,就是view树
添加窗口时通过requestLayout();向ui线程发送消息最后回调到ViewRootImplperformTraversals他是整个ui控件树,measurelayoutdraw的集合
这里分为五个阶段
预测量阶段进行第一次测量,获得viewgetMeasuredWitdh/Height,此时是控件树期望的尺寸会执行View的onMeasure
布局阶段,根据预测量的结果,通过IWindowSessionrelayout向WMS请求调整窗口的尺寸这会使WMS对窗口重新布局,并把结果返回给ViewRootImpl
最终测量阶段, 预测量的结果是view树期望的结果WMS可能会进行调整,在这里WMS已经把结果通知了ViewRootImpl因此这里会窗口实际尺寸performTraversals进行布局view及子类的onMeasure会被回调
布局阶段 测量完成后获得空间的尺寸,布局要确定控件的位置,View及子类的onLayout会被回调
绘制阶段,使用WMS提供的surface进行绘制,View及子类的onDraw会被回调
通常我们看到的都是 先measure,在layout在draw 这里看到其实measure先得到期望值,在和WMS沟通WMS在调整后,返回确定值,在根据确定值进行mesure
measureHierarchy里会通过三次协商执行performMeasure 来确认合适的尺寸
performMeasure 会调用view 的measure 优会调用onMeasure 我们可重写onMeasure来实现测量而measure 方法是final的onMeasure 的结果通过setMeasuredDimension方法保存
对于view onMeasure比较容易 对于ViewGroup则还要遍历调用他所以子view的measure 并且需要考虑padding和子view 的margin padding是控件外内边距 margin 是控件外边距
ViewGroup需要先测量完子view在根据子view的测量值得到自己的宽高举例,如果只有一个子view那么ViewGroup的宽= 子view的宽+子view的margin+viewg的padding 至少是这个值
继续回到performTraversals
这里就是提前测量了一下得到控件树希望的尺寸大小,
通过relayoutWindow来布局窗口 ViewRootImpl 通过IWindowSession 来通知WMS进行窗口布局
这里主要下 调用WMS后WMS会调整窗口的尺寸 同时会生成surface返回给ViewRootImpl 因此后续的绘画就有了画布了可以看到最后的参数是mSurface这是本地的surface 这里会和wms的进行绑定
接下来继续performTraversals,绑定WMS返回的surface然后更新尺寸
最后进行最终测量 上边过程太乱了 了解下就行还是看常见的控件绘制流程
绘制由viewRootImplperformTraversals触发, 抽取出来后,就是这样
就是直接调用view树的根的measure方法 传入到View
该方法是final 意味着无法重写这里又会调用onMeasure
因此对于view在onMeasure中调整好高度,通过setMeasuredDimension设置好自己的测量宽高就可以了
对应ViewGroup则在onMeasure中,先要遍历子view调用他们的measure(注意一定是调用子类的measure,measure又会调用onMeasure), 子view宽高都知道后,在根据子view的宽高来设置自己也就是ViewGroup的宽高受子view影响
可以看到view的measure又调用了onMeasure, 如果是view 则可以直接重新onMeasure来设定大小而对于ViewGroup, 则需要重写onMeasure来先遍历子view设定大小然后再设定viewGroup的大小 ViewGroup并没有重写onMeasure因为每个ViewGroup要实现的效果不同,需要自己完成但ViewGroup提供了几个方法供ViewGroup的继承类来遍历子view
view的宽高由自己的layoutParams和父view提供的 widthMeasureSpec|heightMeasureSpec共同决定
View 自己的宽高,是保存在LayoutParams中对,以宽举例 LayoutParamswidth 有三种情况,精确值(就是指定大小),MATCH_PARENT WRAP_CONTENT,模式则有fuview提供有 unspecified,exactly,at_most三种
匹配如下
其实这个很好理解 如果子view自己指定了宽高就用他的值就可以如果子view是match_parent那就使用父view提供的宽高 如果子view是wrap_content,那就不能超过父view的值
看下ViewGroup为子view绘制而提供的方法,可以看到ViewGroup会减去padding和margin,来提供子view的宽高
上步measure过程未完成后,整个view书的 测量宽高都得到了也就是viewgetMeasuredWidth()和getMeasuredHeight()
performLayout中会调用mViewlayout 这样就把事件从ViewRootImpl传递到了view而layout中又会调用onLayoutViewGroup需要重写onLayout为子view进行布局,遍历调用子view的layout因此就完成整个view树的laylut过程
竖向的实现, 竖向的就行把view从上到下一次排开
这里注意区分measure过程是先得到子view的测量值,在设定父ViewGroup的值而layout过程则是先传入父view的左上右下值,来计算子view的左上右下的位置值这里应该具有普遍性但不知道是否绝对
performDraw 中的调用draw又调用mViewdraw然后就进入view树的绘制了
view的draw 又会调用onDraw ,viewGroup又调用dispatchDraw()把draw分发到子view里 绘制的画布就是canvas 这是从surfacelockCanvas中获得的一个区域
而在ViewGroupdispatchDraw中重要的一点是getChildDrawingOrder 表示子view的绘制顺序默认是与ziview的添加顺序一样我们也可以改变他最后绘制的会显示在最上边,而这也影响view的事件传递顺序
viewdraw 就是一层一层的画内容先画北京,在onDraw在画装饰什么的
canvastranslate(100,300)通过平移坐标系使之后的内容可以直接在新坐标系中绘制
这就是ViewGroup在向子view传递canvas的时候方便多了 会之前先对其ziview的左上角那么子view就可以直接从自己坐标轴的(0,0)开始绘制, 绘制完成后ViewGroup在还原原有坐标系
canvassave canvasrestore 用来保存还原坐标系
viewinvalidate
当某个view发送变化需要重绘时,通过viewinvalidate向上通知到ViewRootImpl从这个view到ViewRootImpl的节点都标记为藏区域dirty area ViewRootimpl再次从上到下重绘时,只绘制这些脏区域效率高
本来安卓兼容使用键盘,也支持,触摸二者的输入事件派发不一样使用键盘时会有个控件处于获得焦点状态处于触摸模式则由用户决定 因此控件分为两类任何情况下都能获得焦点如输入文本框只有在键盘 *** 作时才能获得焦点如菜单,按钮
安卓里有触摸模式当发送任意触摸时进入触摸模式当发送方向键和键盘或者执行ViewrequestRocusFromTouch时,退出触摸模式
获取焦点 viewrequest
先检查是否能获取焦点,
然后设置获取简单的标记,
向上传递到ViewRootimpl保证只能有一个控件获取焦点
通知焦点变化的监听者
更新view的drawable状态,
requestChildFocus会把焦点事件层层上报取消原来有焦点的控件最后的效果就是从viewrootimpl中到最终有焦点的view构成一条 mFoucued 标识的链条来个图就明白了每个view的mFocused总是指向他的直接下级
获取focus的传递是从底层view到顶层的ViewRootImpl而取消focus测试从顶层的ViewRootimpl到底层原来那个获得焦点的view
而如果是ViewGroup请求获取焦点,会根据FLAG_MASK_FOCUSABILITY特性来做不同方式,分别有先让自己获取焦点,或者安卓view的索引递增或者递减来匹配view
ViewRootImpl 中的WindowInputEventReceiver接受输入事件他会把事件包装成一个QueuedInputEvent然后追加到一个单链表的末尾接着重头到尾的处理输入事件,并通过deliverInputEvent完成分发这里会把单链表所有事件都处理完
deliverInput中又会把触摸事件执行到通过 ViewPreImeInputStageprocessKeyEvent 转入mViewdispatchPointerEvent(event)这里又进入 dispatchTouchEvent
MotionEvent是触摸事件的封装getAction可以拿到动作的类型和触控点索引号
getX(),getY()拿到动作的位置信息通过getPointID拿到触控点的id 动作以down 开头跟多个move最后是up
,当事件返回true表示事件被消费掉了
系列文章:
在上篇文章: Android 自定义View之Measure过程 ,我们分析了Measure过程,本次将会掀开承上启下的Layout过程神秘面纱,
通过本篇文章,你将了解到:
在上篇文章的比喻里,我们说过:
该ViewGroup 重写了onMeasure(xx)和onLayout(xx)方法:
同时,当layout 执行结束,清除PFLAG_FORCE_LAYOUT标记,该标记会影响Measure过程是否需要执行onMeasure。
该View 重写了onMeasure(xx)和onLayout(xx)方法:
MyViewGroup里添加了MyView、Button两个控件,最终运行的效果如下:
可以看出,MyViewGroup 里子布局的是横向摆放的。我们重点关注Layout过程。实际上,MyViewGroup里我们只重写了onLayout(xx)方法,MyView也是重写了onLayout(xx)方法。
接下来,分析View Layout过程。
与Measure过程类似,连接ViewGroup onLayout(xx)和View onLayout(xx)之间的桥梁是View layout(xx)。
可以看出,最终都调用了setFrame(xx)方法。
对于Measure过程在onMeasure(xx)里记录了尺寸的值,而对于Layout过程则在layout(xx)里记录了坐标值,具体来说是在setFrame(xx)里,该方法两个重点地方:
ViewonLayout(xx)是空实现
从layout(xx)和onLayout(xx)声明可知,这两个方法都是可以被重写的,接下来看看ViewGroup是否重写了它们。
ViewGrouplayout(xx)虽然重写了layout(xx),但是仅仅做了简单判断,最后还是调用了Viewlayout(xx)。
这重写后将onLayout变为抽象方法,也就是说继承自ViewGroup的类必须重写onLayout(xx)方法。
我们以FrameLayout为例,分析其onLayout(xx)做了什么。
FrameLayoutonLayout(xx)为子布局Layout的时候,起始坐标都是以FrameLayout为基准,并没有记录上一个子布局占了哪块位置,因此子布局的摆放位置可能会重叠,这也是FrameLayout布局特性的由来。而我们之前的Demo在水平方向上记录了上一个子布局的摆放位置,下一个摆放时只能在它之后,因此就形成了水平摆放的功能。
由此类推,我们常说的某个子布局在父布局里的哪个位置,决定这个位置的即是ViewGrouponLayout(xx)。
上边我们分析了Viewlayout(xx)、ViewonLayout(xx)、ViewGrouplayout(xx)、ViewGrouponLayout(xx),这四者什么关系呢?
Viewlayout(xx)
ViewonLayout(xx)
ViewGrouplayout(xx)
ViewGrouponLayout(xx)
View/ViewGroup 子类需要重写哪些方法:
用图表示:
通过上述的描述,我们发现Measure过程和Layout过程里定义的方法比较类似:
它俩的套路比较类似:measure(xx)、layout(xx)一般不需要我们重写,measure(xx)里调用onMeasure(xx),layout(xx)为调用者设置坐标值。
若是ViewGroup:onMeasure(xx)里遍历子布局,并测量每个子布局,最后将结果汇总,设置自己测量的尺寸;onLayout(xx)里遍历子布局,并设置每个子布局的坐标。
若是View:onMeasure(xx)则测量自身,并存储测量尺寸;onLayout(xx)不需要做什么。
Measure过程虽然比Layout过程复杂,但仔细分析后就会发现其本质就是为了设置两个成员变量:
而Layout过程虽然比较简单,其本质是为了设置坐标值
将Measure设置的变量和Layout设置的变量联系起来:
此外,Measure过程通过设置PFLAG_LAYOUT_REQUIRED 标记来告诉需要进行onLayout,而Layout过程通过清除 PFLAG_FORCE_LAYOUT来告诉Measure过程不需要执行onMeasure了。
这就是Layout的承上作用
我们知道View的绘制需要依靠Canvas绘制,而Canvas是有作用区域限制的。例如我们使用:
Cavas绘制的起点是哪呢?
对于硬件绘制加速来说:正是通过Layout过程中设置的RenderNode坐标。
而对于软件绘制来说:
关于硬件绘制加速/软件绘制 后续文章会分析。
这就是Layout的启下作用
以上即是Measure、Layout、Draw三者的内在联系。
当然Layout的"承上"还需要考虑margin、gravity等参数的影响。具体用法参见最开始的Demo。
getMeasuredWidth()/getMeasuredHeight 与 getWidth/getHeight区别
我们以获取width为例,分别来看看其方法:
getMeasuredWidth():获取测量的宽,属于"临时值"
getWidth():获取View真实的宽
在Layout过程之前,getWidth() 默认为0
何时可以获取真实的宽、高
下篇将分析Draw()过程,我们将分析"一切都是draw出来的"道理
本篇基于 Android 100
layouts 的每个子 layout 信息都支持 margins。参考 ViewGroup Margin Layout Attributes 列举了该类支持的所有子 view 的属性。
android:layout_marginBotton
在该 view 的底边指定一个特定的空间。这个空间是在 view 边界的外部。margin 值应该是正数。
必须是一个具体的值,一个浮点数字加一个单位如『145sp』。可选的单位有:px(pixels 象素),dp(density-independent pixels 密度独立象素),sp(scaled pixels based on perferred font size 基于优选字体大小的缩放像素『一般用于设置字体大小』),mm(millimeters 毫米)。
也可能是对包含该类型值的资源或主题属性的引用。
这相当于全局属性资源符号 layout_marginBottom。
相关的方法:
setMargins(int,int,int,int)
android:layout_marginEnd
在该 view 的结束的一边指定一个特定的空间。这个空间是在 view 边界的外部。margin 的值应该是正数。
必须是一个具体的值,一个浮点数字加一个单位如『145sp』。可选的单位有:px(pixels 象素),dp(density-independent pixels 密度独立象素),sp(scaled pixels based on perferred font size 基于优选字体大小的缩放像素『一般用于设置字体大小』),mm(millimeters 毫米)。
也可能是对包含该类型值的资源或主题属性的引用。
这相当于全局属性资源符号 layout_marginEnd。
相关的方法:
setMarginEnd(int)
android_marginLeft
在该 view 的左边指定一个特定的空间。这个空间是在 view 边界的外部。margin 的值应该是正数。
必须是一个具体的值,一个浮点数字加一个单位如『145sp』。可选的单位有:px(pixels 象素),dp(density-independent pixels 密度独立象素),sp(scaled pixels based on perferred font size 基于优选字体大小的缩放像素『一般用于设置字体大小』),mm(millimeters 毫米)。
也可能是对包含该类型值的资源或主题属性的引用。
这相当于全局属性资源符号 layout_marginLeft。
相关的方法:
setMargins(int,int,int,int)
android:layout_marginRight
在该 view 的右边指定一个特定的空间。这个空间是在 view 边界的外部。margin 的值应该是正数。
必须是一个具体的值,一个浮点数字加一个单位如『145sp』。可选的单位有:px(pixels 象素),dp(density-independent pixels 密度独立象素),sp(scaled pixels based on perferred font size 基于优选字体大小的缩放像素『一般用于设置字体大小』),mm(millimeters 毫米)。
也可能是对包含该类型值的资源或主题属性的引用。
这相当于全局属性资源符号 layout_marginRight。
相关的方法:
setMargins(int,int,int,int)
android:layout_marginStart
在该 view 的开始的一边指定一个特定的空间。这个空间是在 view 边界的外部。margin 的值应该是正数。
必须是一个具体的值,一个浮点数字加一个单位如『145sp』。可选的单位有:px(pixels 象素),dp(density-independent pixels 密度独立象素),sp(scaled pixels based on perferred font size 基于优选字体大小的缩放像素『一般用于设置字体大小』),mm(millimeters 毫米)。
也可能是对包含该类型值的资源或主题属性的引用。
这相当于全局属性资源符号 layout_marginStart。
相关的方法:
setMarginStart(int)
android:layout_marginTop
在该 view 的上边指定一个特定的空间。这个空间是在 view 边界的外部。margin 的值应该是正数。
必须是一个具体的值,一个浮点数字加一个单位如『145sp』。可选的单位有:px(pixels 象素),dp(density-independent pixels 密度独立象素),sp(scaled pixels based on perferred font size 基于优选字体大小的缩放像素『一般用于设置字体大小』),mm(millimeters 毫米)。
也可能是对包含该类型值的资源或主题属性的引用。
这相当于全局属性资源符号 layout_marginTop。
相关的方法:
setMargins(int,int,int,int)
public int bottomMargin
以象素为单位的下边距。margin 值应该是正数。调用 setLayoutParams(LayoutParamsp) 方法后为这个值重新分配一个新的值。
public int leftMargin
以象素为单位的左边距。margin 值应该是正数。调用 setLayoutParams(LayoutParamsp) 方法后为这个值重新分配一个新的值。
public int rightMargin
以象素为单位的右边距。margin 值应该是正数。调用 setLayoutParams(LayoutParamsp) 方法后为这个值重新分配一个新的值。
public int topMargin
以象素为单位的上边距。margin 值应该是正数。调用 setLayoutParams(LayoutParamsp) 方法后为这个值重新分配一个新的值。
public ViewGroupMarginLayoutParams(Context c,AttributeSet attrs)
创建一个新的布局参数集合。这些值是从提供的属性集合和 context 中提取的。
参数
c 应用的环境
attrs 来自于布局参数的属性集合
public ViewGroupMarginLayoutParams(int width,int height)
public ViewGroupMarginLayoutParams(ViewGroupMarginLayoutParams source)
复制构造方法。克隆 source 的宽、高和 margin 值。
参数
source 克隆的对象
public int getLayoutDirection()
返回布局的方向。可以是 LAYOUT_DIRECTION_LTR 或是 LAYOUT_DIRECTION_RTL 。
返回值
int 布局的方向
public int getMarginEnd()
返回以象素为单位的结尾 margin 值。
相关的 XML 属性值
android:layout_marginEnd
返回值
int 以象素为单位的结尾 margin 值
public int getMarginStart()
返回以象素为单位的开始的 margin 值。
相关的 XML 属性值
android:layout_marginStart
返回值
int 以象素为单位的开始 margin 值
public boolean isMarginRelative()
检查 margin 是否相对。
相关的 XML 属性
android:layout_marginStart
android:layout_marginEnd
返回值
boolean marginStart 或 marginEnd 已经设定,返回 true
public void resolveLayoutDirection(int layoutDircetion)
由 requestLayout()方法调用。根据布局的方向可以重写左右 margin 的值。
参数
layoutDircetion 布局的方向值(int)
public void setLayoutDircetion(int layoutDirection)
设置布局的方向。
参数
layoutDirection 布局的方向。LAYOUT_DIRECTION_LTR 或是 LAYOUT_DIRECTION_RTL
public void setMarginEnd(int end)
设置相对结束 margin。margin 值应该是正数。
相关的 XML 属性
android:layout_marginEnd
参数
end 结束 margin 的值
public void setMarginStart(int start)
设置相对开始 margin。margin 值应该是正数。
相关的 XML 属性
android:layout_marginStart
参数
start 开始 margin 的值
public void setMargins(int left,int top,int right,int bottom)
设置 margin,以象素为单位。需要先调用 requestLayout() 方法,这样新的 margin 值才能被使用。根据布局的方向左右 margin 值可能会被重写。margin 值应该是正数。
相关的 XML 属性
android:layout_marginLeft
android:layout_marginTop
android:layout_marginRight
android:layout_marginBottom
参数
left 左 margin 值
top 上 margin 值
right 右 margin 值
bottom 下 margin 值
1 初始化
这个其实就是构造函数啦,在这里你可以为这个 view 设置特定的属性啊!那么如何自定义属性呢?首先你得在 res -->
values 这个目录下新建 attrs 的资源文件!在这个文件中配置你要的自定义属性!先看一下代码
<xml version="10" encoding="utf-8">
<resources>
<declare-styleable name="WeekSelectView">
<attr name="selectColor" format="color"></attr>
<attr name="unSelectColor" format="color"></attr>
<attr name="lineColor" format="color"></attr>
<attr name="textColor" format="color"></attr>
<attr name="textSize" format="dimension"></attr>
<attr name="selectSize" format="dimension"></attr>
<attr name="lineWidth" format="dimension"></attr>
<attr name="lineHeight" format="dimension"></attr>
<attr name="space" format="dimension"></attr>
</declare-styleable>
</resources>
其中的 declare-styleable 标签就是添加自定义属性用的,里面包含的每一个 attr 就代表每一个自定义属性!后天面的 format 属性代表每一个属性的类型!接着就是我该如何使用它们呢!我们只要在布局文件中使用代码就行了:
<comkidbotlibrarywidgetweekselectWeekSelectView
xmlns:weekselect=">
1、获取按钮的LayoutParams
LinearLayoutLayoutParams layoutParams = (LinearLayoutLayoutParams)buttongetLayoutParams();2、在LayoutParams中设置margin
layoutParamssetMargins(100,20,10,5);//4个参数按顺序分别是左上右下3、把这个LayoutParams设置给按钮
buttonsetLayoutParams(layoutParams); //mView是控件在工作中经常会遇到d出的dialog有输入框的情况,屏幕大了还好,屏幕小了之后就特别容易出现输入框被软键盘遮住的情况,下面就是我在实际想中中遇到的
从上图可以看出输入框已经看不到了,遇到这种情况的第一个思路都是在dialog的style中添加
<item name="android:windowSoftInputMode">adjustPan</item>,我也试了下基本上没用。然后换了个思路,既然软键盘d出来了,为什么不能让dialog向上移动同样的距离呢。思路有了,下面就是把他实现了。
首先就是要计算软键盘的高度,由于google并没有给出具体的方法来计算软键盘的高度,这时候我们就只能根据布局的高度变化来计算了。首先需要计算出屏幕的bottom坐标,然后监控布局的变动判断变动后的bottom和初始的bottom的差值,一般肉眼观察软键盘的高度差不多是屏幕高度的1/3,所以就假设bottom往上移动了屏幕的1/3的距离就认为软件盘d出来了,当然也可以根据其他值来判断,下面贴出具体方法:
/
activity中判断软键盘是否显示
@param activity
/
fun isKeyboardShowing(activity: Activity): Boolean {
val screenHeight = activitywindow!!decorViewheighttoDouble()
//获取view的可见区域
val rect = Rect()
activitywindow!!decorViewgetWindowVisibleDisplayFrame(rect)
return (20 /30) screenHeight > rectbottomtoDouble()
}
接下来我们来计算出软件盘的高度,经过我在多个测试机上实验发现初始时bottom就是屏幕的高度,下面是计算键盘高度的具体方法
/
activity中计算软键盘的高度
@param activity
/
fun getKeyboardHeight(activity: Activity): Int {
val displayMetrics = DisplayMetrics()
activitywindowManagerdefaultDisplaygetMetrics(displayMetrics)
val screenHeight = displayMetricsheightPixels
val rect = Rect()
activitywindow!!decorViewgetWindowVisibleDisplayFrame(rect)
return screenHeight - rectbottom
}
有了高度之后一切就好办了我们只需要在软键盘d出来的时候把dialog往上移动就行,在移动方式上我选择了设置LayoutParams的方式,开始时想设置底部margin的,结果发现没作用,dialog一点不移动,最后只好设置上边的margin为负值
if (SoftUtilsisKeyboardShowing(context)) {
val lp =mRootViewlayoutParams as ViewGroupMarginLayoutParams
if (lptopMargin ==0) {
lptopMargin = -SoftUtilsgetKeyboardHeight(context)
if (mRootViewheight
lpheight =mRootOriginHeight
}
mRootViewlayoutParams = lp
}
}else {
if (mRootOriginHeight ==0) {
mRootOriginHeight =mRootViewheight
}
val lp =mRootViewlayoutParams as ViewGroupMarginLayoutParams
if (lptopMargin <0) {
lptopMargin =0
mRootViewlayoutParams = lp
}
}
其中mRootView是dialog最外层的布局。在这里面比较重要的一点监测方式,在哪里监测软键盘的d出动作,在activity中可以监测onWindowFocusChanged方法,但是如果封装了dialog的话,dialog中的onWindowFocusChanged并不会起作用,在这里我选择了使用ViewTreeObserver和监听,通过给mRootView的ViewTreeObserver添加addOnGlobalLayoutListener来实时判断,下面是完整的方法
private fun setSpace() {
val treeObserver =mRootViewviewTreeObserver
treeObserveraddOnGlobalLayoutListener{
if (SoftUtilsisKeyboardShowing(context)) {
val lp =mRootViewlayoutParams as ViewGroupMarginLayoutParams
if (lptopMargin ==0) {
lptopMargin = -SoftUtilsgetKeyboardHeight(context)
if (mRootViewheight
lpheight =mRootOriginHeight
}
mRootViewlayoutParams = lp
}
}else {
if (mRootOriginHeight ==0) {
mRootOriginHeight =mRootViewheight
}
val lp =mRootViewlayoutParams as ViewGroupMarginLayoutParams
if (lptopMargin <0) {
lptopMargin =0
mRootViewlayoutParams = lp
}
}
}
}
在这里当软键盘d出的时候重新设置了下dialog的高度,因为有时候软键盘的d出会使dialog的高度压缩,所以d出的时候重新设置下就好了。
这就是我的一个解决思路,当然完全按这个写的话当输入框较多时也可能出问题,最上层的输入框跑屏幕之外去了,这种情况下我们只需要根据输入框的位置动态的计算dialog需要往上移动的距离就行,不要一直设置为计算出来的软键盘的高度。
下图是解决之后的UI
documentbodyclientWidth - documentbodyoffsetWidth // margin的宽
documentbodyclientHeigh - documentbodyoffsetHeight // margin的高
以上就是关于深入理解android2-WMS,控件全部的内容,包括:深入理解android2-WMS,控件、Android 自定义View之Layout过程、ViewGroup.MarginLayoutParams等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)